diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index fdddc02c..ba0a0146 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -34,11 +34,6 @@ jobs: name: Build Backend Binary runs-on: ${{ matrix.os }} - # set working directory - defaults: - run: - working-directory: occupi-backend - steps: - name: Checkout code uses: actions/checkout@v4 @@ -50,22 +45,17 @@ jobs: - name: Build application run: | + cd occupi-backend go build -o occupi-backend_${{ matrix.goos }}_${{ matrix.arch }}${{ matrix.extension }} ${{ matrix.args }} -v cmd/occupi-backend/main.go - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: occupi-backend_${{ matrix.goos }}_${{ matrix.arch }}${{ matrix.extension }} - path: occupi-backend_${{ matrix.goos }}_${{ matrix.arch }}${{ matrix.extension }} + path: occupi-backend/occupi-backend_${{ matrix.goos }}_${{ matrix.arch }}${{ matrix.extension }} build-android: runs-on: ubuntu-latest - - # set working directory - defaults: - run: - working-directory: frontend/occupi-mobile4 - steps: - name: Setup repo uses: actions/checkout@v4 @@ -93,25 +83,19 @@ jobs: token: ${{ secrets.EXPO_TOKEN }} - name: Install dependencies - run: npm install --legacy-peer-deps + run: npm ci - name: Build Android app - run: eas build --platform android --profile preview --local --output ${{ github.workspace }}/occupi-mobile-android.apk + run: eas build --platform android --profile preview --local --output ${{ github.workspace }}/app-release.apk - name: Upload APK artifact uses: actions/upload-artifact@v4 with: - name: occupi-mobile-android.apk - path: occupi-mobile-android.apk + name: app-release + path: ${{ github.workspace }}/app-release.apk build-ios: runs-on: macos-latest - - # set working directory - defaults: - run: - working-directory: frontend/occupi-mobile4 - steps: - name: Setup repo uses: actions/checkout@v4 @@ -130,16 +114,16 @@ jobs: token: ${{ secrets.EXPO_TOKEN }} - name: Install dependencies - run: npm install --legacy-peer-deps + run: npm ci - name: Build iOS app - run: eas build --platform ios --local --non-interactive --output ${{ github.workspace }}/occupi-mobile-ios.ipa + run: eas build --platform ios --local --non-interactive --output ${{ github.workspace }}/app-release.ipa - name: Upload IPA artifact uses: actions/upload-artifact@v4 with: - name: occupi-mobile-ios - path: ${{ github.workspace }}/occupi-mobile-ios.ipa + name: app-release + path: ${{ github.workspace }}/app-release.ipa build-vite-app: name: Build Vite App @@ -168,7 +152,7 @@ jobs: zip -r dist/occupi-web.zip frontend/occupi-web/dist - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: occupi-web path: frontend/occupi-web/dist/occupi-web.zip @@ -197,11 +181,6 @@ jobs: extension: "" args: '' arch: x86_64 - - # set working directory - defaults: - run: - working-directory: frontend/occupi-desktop name: Build Tauri App runs-on: ${{ matrix.os }} @@ -258,10 +237,21 @@ jobs: runs-on: ubuntu-latest needs: [build-backend-binary, build-android, build-ios, build-vite-app, build-tauri-app] steps: - - name: Download artifacts - uses: actions/download-artifact@v4 - # according to the docs, if name is not specified, all artifacts are downloaded - # https://github.com/actions/download-artifact/tree/v4/?tab=readme-ov-file#inputs - with: - path: dist - merge-multiple: true \ No newline at end of file + - name: Install GitHub CLI + run: | + sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key C99B11DEB97541F0 + sudo apt-add-repository https://cli.github.com/packages + sudo apt update + sudo apt install gh + + - name: Login to GitHub + run: | + gh auth login + + - name: Create a Release + run: | + gh release create v1.0.0 -t "v1.0.0" -n "v1.0.0" ./occupi-backend/occupi-backend ./occupi-frontend/occupi-frontend ./occupi-frontend/dist ./occupi-frontend/build ./occupi-frontend/target/release/occupi-frontend + + - name: Upload Release Assets + run: | + gh release upload v1.0.0 ./occupi-backend/occupi-backend ./occupi-frontend/occupi-frontend ./occupi-frontend/dist ./occupi-frontend/build ./occupi-frontend/target/release/occupi-frontend \ No newline at end of file diff --git a/.github/workflows/build-python.yml b/.github/workflows/build-python.yml deleted file mode 100644 index 88c1999a..00000000 --- a/.github/workflows/build-python.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Build Python App πŸ—οΈ - -on: - pull_request: - branches: ["develop"] - paths: [ - "python-code/**", - ".github/workflows/build-python.yml" - ] - - workflow_dispatch: - -defaults: - run: - working-directory: python-code - -jobs: - build: - name: πŸ—οΈ Build - runs-on: ubuntu-latest - - strategy: - matrix: - python-version: [3.8, 3.9, 3.11] - - steps: - - name: ⬇️ Checkout repository - uses: actions/checkout@v4 - - - name: πŸ— Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: πŸ“¦ Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - - name: βœ… Build completed - run: echo "Build completed successfully!" diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index fc9d68a7..e5603fe6 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,4 +1,4 @@ -name: Build πŸ—οΈ and Deploy Documentation πŸ›³οΈ +name: Deploy Docs site to Live site on: push: @@ -10,8 +10,7 @@ on: "documentation/occupi-docs/next.config.js", "documentation/occupi-docs/package.json", "documentation/occupi-docs/theme.config.tsx", - "documentation/occupi-docs/tsconfig.json", - ".github/workflows/deploy-docs.yml" + "documentation/occupi-docs/tsconfig.json" ] workflow_dispatch: @@ -23,45 +22,44 @@ defaults: jobs: # Build job build-test: - name: πŸ—οΈ Build runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout + - name: Checkout uses: actions/checkout@v4 - - name: πŸ— Setup Bun + - name: Setup Bun uses: oven-sh/setup-bun@v1 with: bun-version: latest # or "latest", "canary", - - name: πŸ“¦ Install dependencies with Bun + - name: Install dependencies with Bun run: bun install - - name: πŸš€ Build with Next.js + - name: Build with Next.js run: bun run build build-push-docker: - name: πŸ‹ Build and Push Documentation Docker Image + name: Build and Push Documentation Docker Image runs-on: ubuntu-latest needs: build-test steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸ— Set up QEMU + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: πŸ— Set up Docker Buildx + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: πŸ§‘β€πŸ’» Login to DockerHub + - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: 🐳 Build and push Docker image + - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: documentation/occupi-docs @@ -71,15 +69,15 @@ jobs: tags: ${{ secrets.DOCKER_USERNAME }}/occupi-documentation:latest deploy: - name: πŸ›³οΈ Deploy Documentation + name: Deploy Documentation runs-on: ubuntu-latest needs: build-push-docker steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸͺ· Copy files to VM + - name: Copy files to VM uses: appleboy/scp-action@v0.1.5 with: host: ${{ secrets.VM_IP }} @@ -89,7 +87,7 @@ jobs: target: "/home/${{ secrets.VM_USERNAME }}/occupi-docs" # SSH to VM and run commands - - name: πŸš€ SSH to VM + - name: SSH to VM uses: appleboy/ssh-action@master with: host: ${{ secrets.VM_IP }} diff --git a/.github/workflows/deploy-golang-develop.yml b/.github/workflows/deploy-golang-develop.yml index 6549a8dd..d8a2b940 100644 --- a/.github/workflows/deploy-golang-develop.yml +++ b/.github/workflows/deploy-golang-develop.yml @@ -1,4 +1,4 @@ -name: Build πŸ—οΈ and Deploy Golang App πŸ›³οΈ to Develop 🚈 +name: Build and Deploy Dev golang on: push: @@ -18,52 +18,52 @@ defaults: jobs: build-test: - name: πŸ—οΈ Build + name: Build runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸ— Set up Go + - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.21' # Specify the Go version you are using - - name: πŸš€ Build the code + - name: Build the code run: | go build -v cmd/occupi-backend/main.go build-push-docker: - name: πŸ‹ Build and Push Develop Docker Image + name: Build and Push Develop Docker Image runs-on: ubuntu-latest needs: build-test steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸ— Set up QEMU + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: πŸ— Set up Docker Buildx + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: πŸ§‘β€πŸ’» Login to DockerHub + - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: πŸ”“ Decrypt default variables + - name: Decrypt default variables run: | echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/config.yaml.gpg > configs/config.yaml - - name: πŸ”“ Decrypt test variables + - name: Decrypt test variables run: | echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/dev.deployed.yaml.gpg > configs/dev.deployed.yaml - - name: 🐳 Build and push Docker image + - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: occupi-backend @@ -73,15 +73,15 @@ jobs: tags: ${{ secrets.DOCKER_USERNAME }}/occupi-backend:latest-develop deploy: - name: πŸ›³οΈ Deploy for Develop + name: Deploy for Develop runs-on: ubuntu-latest needs: build-push-docker steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸͺ· Copy files to VM + - name: Copy files to VM uses: appleboy/scp-action@v0.1.5 with: host: ${{ secrets.VM_IP }} @@ -91,7 +91,7 @@ jobs: target: "/home/${{ secrets.VM_USERNAME }}/occupi-backend-dev" # SSH to VM and run commands - - name: πŸš€ SSH to VM + - name: SSH to VM uses: appleboy/ssh-action@master with: host: ${{ secrets.VM_IP }} diff --git a/.github/workflows/deploy-golang-prod.yml b/.github/workflows/deploy-golang-prod.yml index b9a18663..1babe0cf 100644 --- a/.github/workflows/deploy-golang-prod.yml +++ b/.github/workflows/deploy-golang-prod.yml @@ -1,4 +1,4 @@ -name: BuildπŸ—οΈ and Deploy Golang App πŸ›³οΈ to Prod 🚝 +name: Build and Deploy Prod golang on: push: @@ -13,52 +13,52 @@ defaults: jobs: build: - name: πŸ—οΈ Build + name: Build runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸ— Set up Go + - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.21' # Specify the Go version you are using - - name: πŸš€ Build the code + - name: Build the code run: | go build -v cmd/occupi-backend/main.go build-push-docker: needs: build - name: πŸ‹ Build and Push Master Docker Image + name: Build and Push Master Docker Image runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸ— Set up QEMU + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: πŸ— Set up Docker Buildx + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: πŸ§‘β€πŸ’» Login to DockerHub + - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: πŸ”“ Decrypt default variables + - name: Decrypt default variables run: | echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/config.yaml.gpg > configs/config.yaml - - name: πŸ”“ Decrypt test variables + - name: Decrypt test variables run: | echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/prod.yaml.gpg > configs/prod.yaml - - name: 🐳 Build and push Docker image + - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: occupi-backend @@ -69,14 +69,14 @@ jobs: deploy: needs: build-push-docker - name: πŸ›³οΈ Deploy for Prod + name: Deploy for Prod runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸͺ· Copy files to VM + - name: Copy files to VM uses: appleboy/scp-action@v0.1.5 with: host: ${{ secrets.VM_IP }} @@ -86,7 +86,7 @@ jobs: target: "/home/${{ secrets.VM_USERNAME }}/occupi-backend-prod" # SSH to VM and run commands - - name: πŸš€ SSH to VM + - name: SSH to VM uses: appleboy/ssh-action@master with: host: ${{ secrets.VM_IP }} diff --git a/.github/workflows/deploy-landing-page.yml b/.github/workflows/deploy-landing-page.yml index 648b6502..345c3f23 100644 --- a/.github/workflows/deploy-landing-page.yml +++ b/.github/workflows/deploy-landing-page.yml @@ -1,11 +1,10 @@ -name: Build πŸ—οΈ and Deploy Landing Page πŸ›³οΈ +name: Deploy Landing page on: push: branches: ["develop"] paths: [ "frontend/occupi/**", - ".github/workflows/deploy-landing-page.yml" ] workflow_dispatch: @@ -17,45 +16,44 @@ defaults: jobs: # Build job build-test: - name: πŸ—οΈ Build runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout + - name: Checkout uses: actions/checkout@v4 - - name: πŸ— Setup Bun + - name: Setup Bun uses: oven-sh/setup-bun@v1 with: bun-version: latest # or "latest", "canary", - - name: πŸ“¦ Install dependencies with Bun + - name: Install dependencies with Bun run: bun install - - name: πŸš€ Build with Next.js + - name: Build with Next.js run: bun run build build-push-docker: - name: πŸ‹ Build and Push Landing Page Docker Image + name: Build and Push Landing Page Docker Image runs-on: ubuntu-latest needs: build-test steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸ— Set up QEMU + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: πŸ— Set up Docker Buildx + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: πŸ§‘β€πŸ’» Login to DockerHub + - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: 🐳 Build and push Docker image + - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: frontend/occupi @@ -65,15 +63,15 @@ jobs: tags: ${{ secrets.DOCKER_USERNAME }}/occupi:latest deploy: - name: πŸ›³οΈ Deploy Landing Page + name: Deploy Landing Page runs-on: ubuntu-latest needs: build-push-docker steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸͺ· Copy files to VM + - name: Copy files to VM uses: appleboy/scp-action@v0.1.5 with: host: ${{ secrets.VM_IP }} @@ -83,7 +81,7 @@ jobs: target: "/home/${{ secrets.VM_USERNAME }}/occupi" # SSH to VM and run commands - - name: πŸš€ SSH to VM + - name: SSH to VM uses: appleboy/ssh-action@master with: host: ${{ secrets.VM_IP }} diff --git a/.github/workflows/deploy-web-develop.yml b/.github/workflows/deploy-web-develop.yml index e966bb20..57e0436d 100644 --- a/.github/workflows/deploy-web-develop.yml +++ b/.github/workflows/deploy-web-develop.yml @@ -1,11 +1,10 @@ -name: Build πŸ—οΈ and Deploy Web πŸ›³οΈ to Develop 🚈 +name: Deploy Develop Dashboard on: push: branches: ["develop"] paths: [ "frontend/occupi-web/**", - ".github/workflows/deploy-web-develop.yml" ] workflow_dispatch: @@ -17,49 +16,48 @@ defaults: jobs: # Build job build-test: - name: πŸ—οΈ Build runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout + - name: Checkout uses: actions/checkout@v4 - - name: πŸ— Setup Bun + - name: Setup Bun uses: oven-sh/setup-bun@v1 with: bun-version: latest # or "latest", "canary", - - name: πŸ“¦ Install dependencies with Bun + - name: Install dependencies with Bun run: bun install - - name: πŸš€ Build with Vite.js + - name: Build with Vite.js run: bun run build build-push-docker: - name: πŸ‹ Build and Push Web Docker Image + name: Build and Push Web Docker Image runs-on: ubuntu-latest needs: build-test steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 # run npm install so we can get package-lock.json file since it's not in the repo due to bun - - name: πŸ“¦ Install dependencies + - name: Install dependencies run: npm install - - name: πŸ— Set up QEMU + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: πŸ— Set up Docker Buildx + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: πŸ§‘β€πŸ’» Login to DockerHub + - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: 🐳 Build and push Docker image + - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: frontend/occupi-web @@ -69,15 +67,15 @@ jobs: tags: ${{ secrets.DOCKER_USERNAME }}/occupi-web:latest-develop deploy: - name: πŸ›³οΈ Deploy Web + name: Deploy Web runs-on: ubuntu-latest needs: build-push-docker steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸͺ· Copy files to VM + - name: Copy files to VM uses: appleboy/scp-action@v0.1.5 with: host: ${{ secrets.VM_IP }} @@ -87,7 +85,7 @@ jobs: target: "/home/${{ secrets.VM_USERNAME }}/occupi-web-dev" # SSH to VM and run commands - - name: πŸš€ SSH to VM + - name: SSH to VM uses: appleboy/ssh-action@master with: host: ${{ secrets.VM_IP }} diff --git a/.github/workflows/deploy-web-prod.yml b/.github/workflows/deploy-web-prod.yml index e6917e3f..93264ec8 100644 --- a/.github/workflows/deploy-web-prod.yml +++ b/.github/workflows/deploy-web-prod.yml @@ -1,4 +1,4 @@ -name: Build πŸ—οΈ and Deploy πŸ›³οΈ Web App to Production 🚝 +name: Deploy Prod Dashboard on: push: @@ -16,49 +16,48 @@ defaults: jobs: # Build job build-test: - name: πŸ—οΈ Build runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout + - name: Checkout uses: actions/checkout@v4 - - name: πŸ— Setup Bun + - name: Setup Bun uses: oven-sh/setup-bun@v1 with: bun-version: latest # or "latest", "canary", - - name: πŸ“¦ Install dependencies with Bun + - name: Install dependencies with Bun run: bun install - - name: πŸš€ Build with Vite.js + - name: Build with Vite.js run: bun run build build-push-docker: - name: πŸ‹ Build and Push Web Docker Image + name: Build and Push Web Docker Image runs-on: ubuntu-latest needs: build-test steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 # run npm install so we can get package-lock.json file since it's not in the repo due to bun - - name: πŸ“¦ Install dependencies + - name: Install dependencies run: npm install - - name: πŸ— Set up QEMU + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: πŸ— Set up Docker Buildx + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: πŸ§‘β€πŸ’» Login to DockerHub + - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: 🐳 Build and push Docker image + - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: frontend/occupi-web @@ -68,15 +67,15 @@ jobs: tags: ${{ secrets.DOCKER_USERNAME }}/occupi-web:latest deploy: - name: πŸ›³οΈ Deploy Web + name: Deploy Web runs-on: ubuntu-latest needs: build-push-docker steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸͺ· Copy files to VM + - name: Copy files to VM uses: appleboy/scp-action@v0.1.5 with: host: ${{ secrets.VM_IP }} @@ -86,7 +85,7 @@ jobs: target: "/home/${{ secrets.VM_USERNAME }}/occupi-web-prod" # SSH to VM and run commands - - name: πŸš€ SSH to VM + - name: SSH to VM uses: appleboy/ssh-action@master with: host: ${{ secrets.VM_IP }} diff --git a/.github/workflows/lint-test-build-golang.yml b/.github/workflows/lint-test-build-golang.yml index 26cf8e3a..43f90d83 100644 --- a/.github/workflows/lint-test-build-golang.yml +++ b/.github/workflows/lint-test-build-golang.yml @@ -1,4 +1,4 @@ -name: Lint🌸, TestπŸ§ͺ and BuildπŸ—οΈ golang +name: Lint, Test and Build golang on: pull_request: @@ -20,28 +20,28 @@ defaults: jobs: lint: - name: 🌸 Lint + name: Lint runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸ— Set up Go + - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.21' # Specify the Go version you are using - - name: πŸ“¦ Install golangci-lint + - name: Install golangci-lint run: | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - - name: 🌸 Run golangci-lint + - name: Run golangci-lint run: | golangci-lint run test: - name: πŸ§ͺ Test + name: Test runs-on: ubuntu-latest services: @@ -56,10 +56,10 @@ jobs: --health-retries 5 steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸ“¦ Install mongosh + - name: Install mongosh run: | wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo apt-key add - echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/5.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-5.0.list @@ -67,7 +67,7 @@ jobs: sudo apt-get install -y mongodb-mongosh - - name: βŒ› Wait for MongoDB to be ready + - name: Wait for MongoDB to be ready run: | for i in {1..30}; do if mongosh --eval 'db.adminCommand({ping: 1})' ${{ secrets.MONGO_DB_TEST_URL }}; then @@ -78,7 +78,7 @@ jobs: sleep 2 done - - name: πŸ— Create MongoDB User + - name: Create MongoDB User env: MONGO_INITDB_ROOT_USERNAME: ${{ secrets.MONGO_DB_TEST_USERNAME }} MONGO_INITDB_ROOT_PASSWORD: ${{ secrets.MONGO_DB_TEST_PASSWORD }} @@ -94,47 +94,45 @@ jobs: }); " - - name: πŸ— Set up Go + - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.21' # Specify the Go version you are using - - name: πŸ”“ Decrypt default variables + - name: Decrypt default variables run: | echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/config.yaml.gpg > configs/config.yaml - - name: πŸ”“ Decrypt test variables + - name: Decrypt test variables run: | echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/test.yaml.gpg > configs/test.yaml - - name: πŸ“¦ Install gotestsum + - name: Install gotestsum run: | go install gotest.tools/gotestsum@latest - - name: πŸ§ͺ Run tests + - name: Run tests run: | gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out - - name: πŸ“‹ Upload coverage reports to Codecov + - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true - verbose: true build: - name: πŸ—οΈ Build + name: Build runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout code + - name: Checkout code uses: actions/checkout@v4 - - name: πŸ— Set up Go + - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.21' # Specify the Go version you are using - - name: πŸš€ Build the code + - name: Build the code run: | go build -v cmd/occupi-backend/main.go diff --git a/.github/workflows/lint-test-build-web.yml b/.github/workflows/lint-test-build-web.yml index 1d8e3c15..a1f26d48 100644 --- a/.github/workflows/lint-test-build-web.yml +++ b/.github/workflows/lint-test-build-web.yml @@ -1,11 +1,10 @@ -name: Lint🌸, TestπŸ§ͺ, BuildπŸ—οΈ Web +name: Lint, Test, Build Web on: pull_request: branches: ["develop"] paths: [ "frontend/occupi-web/**", - ".github/workflows/lint-test-build-web.yml" ] workflow_dispatch: @@ -17,65 +16,59 @@ defaults: jobs: # Lint job lint: - name: 🌸 Lint runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout + - name: Checkout uses: actions/checkout@v4 - - name: πŸ— Setup Bun + - name: Setup Bun uses: oven-sh/setup-bun@v1 with: bun-version: latest # or "latest", "canary", - - name: πŸ“¦ Install dependencies with Bun + - name: Install dependencies with Bun run: bun install - - name: 🌸 Lint with ESLint + - name: Lint with ESLint run: bun run lint # Test job test: - name: πŸ§ͺ Test runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout + - name: Checkout uses: actions/checkout@v4 - - name: πŸ— Setup Bun + - name: Setup Bun uses: oven-sh/setup-bun@v1 with: bun-version: latest # or "latest", "canary", - - name: πŸ“¦ Install dependencies with Bun + - name: Install dependencies with Bun run: bun install - - name: πŸ§ͺ Test with Jest - run: bun test --coverage --coverage-reporter=lcov --coverage-dir=coverage + - name: Test with Jest + run: bun test --coverage - - name: πŸ“‹ Upload coverage reports to Codecov + - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage/lcov.info - fail_ci_if_error: true - verbose: true # Build job build: - name: πŸ—οΈ Build runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout + - name: Checkout uses: actions/checkout@v4 - - name: πŸ— Setup Bun + - name: Setup Bun uses: oven-sh/setup-bun@v1 with: bun-version: latest # or "latest", "canary", - - name: πŸ“¦ Install dependencies with Bun + - name: Install dependencies with Bun run: bun install - - name: πŸš€ Build with Next.js + - name: Build with Next.js run: bun run build diff --git a/.github/workflows/lint-test-mobile.yml b/.github/workflows/lint-test-mobile.yml index 3298f2c1..6219cc58 100644 --- a/.github/workflows/lint-test-mobile.yml +++ b/.github/workflows/lint-test-mobile.yml @@ -1,4 +1,4 @@ -name: Lint🌸 and TestπŸ§ͺ Mobile +name: Lint and Test Mobile on: pull_request: @@ -17,47 +17,37 @@ defaults: jobs: lint: - name: 🌸 Lint runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout Repository + - name: Checkout Repository uses: actions/checkout@v4 - - name: πŸ— Set up Node.js + - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 18 - - name: πŸ“¦ Install Dependencies + - name: Install Dependencies run: npm install --legacy-peer-deps - - name: 🌸 Lint + - name: Lint run: npm run lint test: - name: πŸ§ͺ Test runs-on: ubuntu-latest steps: - - name: ⬇️ Checkout Repository + - name: Checkout Repository uses: actions/checkout@v4 - - name: πŸ— Set up Node.js + - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 18 - - name: πŸ“¦ Install Dependencies + - name: Install Dependencies run: npm install --legacy-peer-deps - - name: πŸ§ͺ Run Tests - run: npm run test:coverage - - - name: πŸ“‹ Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.0.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage/lcov.info - fail_ci_if_error: true - verbose: true \ No newline at end of file + - name: Run Tests + run: npm run test \ No newline at end of file diff --git a/.github/workflows/test-and-cov.yml b/.github/workflows/test-and-cov.yml deleted file mode 100644 index 1753b4b3..00000000 --- a/.github/workflows/test-and-cov.yml +++ /dev/null @@ -1,173 +0,0 @@ -name: TestπŸ§ͺ WebπŸ’», MobileπŸ“± and APIπŸ”Œ and generate coverage reportπŸ“‹ - -on: - push: - branches: ["develop"] - paths: [ - "occupi-backend/cmd/**", - "occupi-backend/configs/**", - "occupi-backend/pkg/**", - "occupi-backend/.golangci.yml", - "occupi-backend/tests/**", - ".github/workflows/test-and-cov.yml", - "frontend/occupi-web/**", - "frontend/occupi-mobile4/**", - ".github/workflows/lint-test-build-web.yml", - ".github/workflows/lint-test-build-golang.yml", - ".github/workflows/lint-test-mobile.yml" - ] - - workflow_dispatch: - -jobs: - test-api: - name: πŸ§ͺ Test API πŸ”Œ - runs-on: ubuntu-latest - - # set working directory - defaults: - run: - working-directory: occupi-backend - - services: - mongo: - image: mongo:latest - ports: - - 27017:27017 - options: >- - --health-cmd "mongosh --eval 'db.adminCommand({ping: 1})'" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - - name: ⬇️ Checkout code - uses: actions/checkout@v4 - - - name: πŸ“¦ Install mongosh - run: | - wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo apt-key add - - echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/5.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-5.0.list - sudo apt-get update - sudo apt-get install -y mongodb-mongosh - - - - name: βŒ› Wait for MongoDB to be ready - run: | - for i in {1..30}; do - if mongosh --eval 'db.adminCommand({ping: 1})' ${{ secrets.MONGO_DB_TEST_URL }}; then - echo "MongoDB is up" - break - fi - echo "Waiting for MongoDB to be ready..." - sleep 2 - done - - - name: πŸ— Create MongoDB User - env: - MONGO_INITDB_ROOT_USERNAME: ${{ secrets.MONGO_DB_TEST_USERNAME }} - MONGO_INITDB_ROOT_PASSWORD: ${{ secrets.MONGO_DB_TEST_PASSWORD }} - MONGO_INITDB_DATABASE: ${{ secrets.MONGO_DB_TEST_DB }} - run: | - mongosh ${{ secrets.MONGO_DB_TEST_URL }}/admin --eval " - db.createUser({ - user: '${MONGO_INITDB_ROOT_USERNAME}', - pwd: '${MONGO_INITDB_ROOT_PASSWORD}', - roles: [ - { role: 'readWrite', db: '${MONGO_INITDB_DATABASE}' } - ] - }); - " - - - name: πŸ— Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.21' # Specify the Go version you are using - - - name: πŸ”“ Decrypt default variables - run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/config.yaml.gpg > configs/config.yaml - - - name: πŸ”“ Decrypt test variables - run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/test.yaml.gpg > configs/test.yaml - - - name: πŸ“¦ Install gotestsum - run: | - go install gotest.tools/gotestsum@latest - - - name: πŸ§ͺ Run tests - run: | - gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out - - - name: πŸ“‹ Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.0.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true - verbose: true - - # Test job - test-web: - name: πŸ§ͺ Test web πŸ’» - runs-on: ubuntu-latest - - # set working directory - defaults: - run: - working-directory: frontend/occupi-web - - steps: - - name: ⬇️ Checkout - uses: actions/checkout@v4 - - - name: πŸ— Setup Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest # or "latest", "canary", - - - name: πŸ“¦ Install dependencies with Bun - run: bun install - - - name: πŸ§ͺ Test with Jest - run: bun test --coverage --coverage-reporter=lcov --coverage-dir=coverage - - - name: πŸ“‹ Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.0.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage/lcov.info - fail_ci_if_error: true - verbose: true - - test-mobile: - name: πŸ§ͺ Test mobile πŸ“± - runs-on: ubuntu-latest - - # set working directory - defaults: - run: - working-directory: frontend/occupi-mobile4 - - steps: - - name: ⬇️ Checkout Repository - uses: actions/checkout@v4 - - - name: πŸ— Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 18 - - - name: πŸ“¦ Install Dependencies - run: npm install --legacy-peer-deps - - - name: πŸ§ͺ Run Tests - run: npm run test:coverage - - - name: πŸ“‹ Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.0.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage/lcov.info - fail_ci_if_error: true - verbose: true diff --git a/.github/workflows/test-mobile.yml b/.github/workflows/test-mobile.yml new file mode 100644 index 00000000..b24b27c5 --- /dev/null +++ b/.github/workflows/test-mobile.yml @@ -0,0 +1,34 @@ +name: Test Mobile + +on: + pull_request: + branches: + - develop + paths: [ + "frontend/occupi-mobile3/**" + ] + + workflow_dispatch: + +defaults: + run: + working-directory: frontend/occupi-mobile3 + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install Dependencies + run: npm install --legacy-peer-deps + + - name: Run Tests + run: npm run test-ci \ No newline at end of file diff --git a/datasets/Office_Visitations.csv b/datasets/Office_Visitations.csv new file mode 100644 index 00000000..96f49114 --- /dev/null +++ b/datasets/Office_Visitations.csv @@ -0,0 +1,1001 @@ +id,Employee Name,Department,Visit_Date,Entry_Time,Exit_time,Purpose_of_visit,Day_of_Week +1,Trix Lanyon,Marketing,2024-12-01,7:46:57,08:46:57,Meeting,Sunday +2,Kendall Chuney,Marketing,2024-12-04,6:44:09,13:44:09,Training,Wednesday +3,Lilly Issacson,Research and Development,2024-12-03,9:07:14,15:07:14,Work,Tuesday +4,Carolee Dudleston,Business Development,2024-02-01,7:06:43,12:06:43,Meeting,Thursday +5,Cacilia Partrick,Engineering,2024-08-01,9:13:55,17:13:55,Meeting,Thursday +6,Reyna Huglin,Sales,2024-05-07,9:02:01,12:02:01,Training,Tuesday +7,Artemis Vivian,Human Resources,2024-08-01,9:19:09,13:19:09,Work,Thursday +8,Wait Petrillo,Training,2024-12-04,8:05:31,10:05:31,Work,Wednesday +9,Manon Norree,Product Management,2024-11-05,7:31:53,08:31:53,Meeting,Tuesday +10,Laura Trevithick,Research and Development,2024-07-01,6:24:16,14:24:16,Other,Monday +11,Evered Hartill,Services,2024-04-03,9:29:56,10:29:56,Meeting,Wednesday +12,Pammi Seacroft,Legal,2024-01-02,8:14:57,11:14:57,Client Visit,Tuesday +13,Cacilie Loney,Accounting,2024-02-01,8:17:53,16:17:53,Work,Thursday +14,Cilka Porte,Business Development,2024-06-03,9:30:36,11:30:36,Work,Monday +15,Dottie Cunradi,Support,2024-12-02,6:08:39,13:08:39,Other,Monday +16,Adore Foulds,Legal,2024-08-05,7:30:17,09:30:17,Meeting,Monday +17,Lina Ovington,Human Resources,2024-11-04,7:57:47,13:57:47,Work,Monday +18,Tallulah Tresler,Accounting,2024-07-03,9:34:16,10:34:16,Training,Wednesday +19,Godwin Benninck,Product Management,2024-09-06,6:29:14,14:29:14,Other,Friday +20,Dyan Menis,Accounting,2024-04-05,6:41:31,12:41:31,Work,Friday +21,Emmalynne Hospital,Engineering,2024-02-03,8:37:39,12:37:39,Meeting,Saturday +22,Elisabetta Klemmt,Research and Development,2024-08-05,7:26:14,09:26:14,Client Visit,Monday +23,Rory Grishanin,Legal,2024-02-03,7:56:05,12:56:05,Work,Saturday +24,Jennilee Gardener,Product Management,2024-05-02,8:30:19,11:30:19,Client Visit,Thursday +25,Caren Guilbert,Support,2024-08-03,9:35:56,13:35:56,Work,Saturday +26,Orson Battersby,Engineering,2024-08-01,6:14:38,07:14:38,Work,Thursday +27,Shari Forst,Accounting,2024-12-05,6:43:37,09:43:37,Other,Thursday +28,Kelli Oldroyd,Sales,2024-07-02,8:02:24,13:02:24,Client Visit,Tuesday +29,Martyn Walcot,Support,2024-04-05,6:25:26,10:25:26,Work,Friday +30,Jase Kienl,Engineering,2024-11-07,6:47:30,07:47:30,Meeting,Thursday +31,Harmonie Jamson,Human Resources,2024-02-01,9:41:28,16:41:28,Work,Thursday +32,Alberto Escott,Business Development,2024-04-07,9:42:45,11:42:45,Meeting,Sunday +33,Reggy Philippault,Services,2024-02-04,7:45:34,14:45:34,Other,Sunday +34,Donalt Hartright,Research and Development,2024-11-04,8:54:38,10:54:38,Meeting,Monday +35,Mackenzie Croci,Research and Development,2024-12-04,9:11:11,11:11:11,Work,Wednesday +36,Clark Laverenz,Support,2024-10-01,7:28:07,13:28:07,Work,Tuesday +37,Joey Crystal,Legal,2024-11-02,6:35:00,14:35:00,Meeting,Saturday +38,Helge Gribbell,Business Development,2024-08-04,6:21:11,10:21:11,Work,Sunday +39,Andris Corre,Services,2024-01-01,7:49:24,12:49:24,Other,Monday +40,Delano Rozzier,Legal,2024-12-01,9:18:49,15:18:49,Client Visit,Sunday +41,Chrotoem Bertelmot,Accounting,2024-04-01,9:04:38,10:04:38,Client Visit,Monday +42,Vidovik Doeg,Research and Development,2024-04-03,9:21:14,12:21:14,Work,Wednesday +43,Ferris Nazareth,Support,2024-05-03,8:39:00,12:39:00,Work,Friday +44,Sharlene Shilliday,Marketing,2024-01-07,7:36:25,12:36:25,Meeting,Sunday +45,Stephani Sexten,Product Management,2024-07-02,8:56:43,13:56:43,Client Visit,Tuesday +46,Fields Kliement,Product Management,2024-11-07,9:57:12,11:57:12,Other,Thursday +47,Jayme O'Cuddie,Research and Development,2024-02-04,6:04:24,10:04:24,Meeting,Sunday +48,Florry Headington,Accounting,2024-04-04,9:41:56,14:41:56,Training,Thursday +49,Marabel Gapper,Training,2024-07-02,6:02:41,13:02:41,Work,Tuesday +50,Willow Logg,Sales,2024-06-05,7:11:22,13:11:22,Meeting,Wednesday +51,Donaugh Breffit,Accounting,2024-02-04,7:19:21,12:19:21,Client Visit,Sunday +52,Deirdre Eads,Services,2024-05-01,8:39:45,15:39:45,Client Visit,Wednesday +53,Frederik Gibling,Research and Development,2024-02-02,8:05:10,15:05:10,Work,Friday +54,Nickola Scarborough,Training,2024-12-03,8:02:54,10:02:54,Client Visit,Tuesday +55,Callie Wyre,Research and Development,2024-02-01,7:02:06,10:02:06,Meeting,Thursday +56,Padraic Quantrell,Engineering,2024-07-06,8:08:19,15:08:19,Meeting,Saturday +57,Oliver Yaneev,Sales,2024-06-03,8:20:23,10:20:23,Work,Monday +58,Evelina Hestrop,Legal,2024-12-05,7:37:38,14:37:38,Work,Thursday +59,Corby Benda,Training,2024-08-06,8:46:18,16:46:18,Work,Tuesday +60,Bethena Nijs,Services,2024-02-01,9:21:35,15:21:35,Client Visit,Thursday +61,Cammy Casacchia,Business Development,2024-04-07,8:44:00,14:44:00,Meeting,Sunday +62,Morley Wrightem,Research and Development,2024-11-06,8:22:30,16:22:30,Work,Wednesday +63,Antoine Malatalant,Engineering,2024-10-07,8:37:32,15:37:32,Meeting,Monday +64,Theodore Darque,Engineering,2024-04-05,6:57:07,13:57:07,Meeting,Friday +65,Deb Bernard,Engineering,2024-11-05,6:15:49,14:15:49,Work,Tuesday +66,Gale Matzke,Marketing,2024-12-07,9:44:02,17:44:02,Work,Saturday +67,Raleigh Aluard,Marketing,2024-03-01,8:51:09,13:51:09,Other,Friday +68,Kori Jenson,Research and Development,2024-06-07,7:28:28,09:28:28,Work,Friday +69,Korry Reboul,Accounting,2024-05-06,8:38:31,09:38:31,Meeting,Monday +70,Hilary Haggie,Training,2024-08-02,6:21:28,10:21:28,Work,Friday +71,Christiano Philbrick,Research and Development,2024-07-04,6:27:40,08:27:40,Other,Thursday +72,Charyl Chetham,Sales,2024-03-02,9:10:52,13:10:52,Work,Saturday +73,Willis Dryburgh,Marketing,2024-08-07,6:44:43,08:44:43,Meeting,Wednesday +74,Emmye Phipp,Training,2024-06-03,8:25:18,16:25:18,Meeting,Monday +75,Germaine Girone,Business Development,2024-04-02,7:38:42,15:38:42,Work,Tuesday +76,Dacey Broadbere,Business Development,2024-01-07,7:32:22,10:32:22,Meeting,Sunday +77,Windy Haycraft,Marketing,2024-06-06,9:51:22,14:51:22,Other,Thursday +78,Isabelle Greensall,Support,2024-09-01,8:00:15,11:00:15,Meeting,Sunday +79,Gipsy Parsons,Product Management,2024-07-04,8:58:47,09:58:47,Meeting,Thursday +80,Jess Izod,Legal,2024-01-02,8:27:00,12:27:00,Meeting,Tuesday +81,Roma Livingstone,Training,2024-10-07,8:21:26,14:21:26,Training,Monday +82,Julina Van Leijs,Sales,2024-07-04,9:37:32,13:37:32,Meeting,Thursday +83,Hulda Praten,Legal,2024-01-03,6:43:47,09:43:47,Work,Wednesday +84,Marcela Boshere,Sales,2024-04-01,6:16:21,09:16:21,Work,Monday +85,Nappy Scarisbrick,Marketing,2024-06-02,9:58:38,11:58:38,Meeting,Sunday +86,Zorana Hankins,Accounting,2024-10-04,6:58:40,11:58:40,Other,Friday +87,Gizela Daintith,Marketing,2024-05-07,6:12:27,07:12:27,Training,Tuesday +88,Rora Pantridge,Accounting,2024-05-05,6:30:31,10:30:31,Meeting,Sunday +89,Brinna Kuzemka,Sales,2024-05-01,6:17:53,08:17:53,Client Visit,Wednesday +90,Kris Maginot,Engineering,2024-09-05,8:25:09,16:25:09,Work,Thursday +91,Baily Cotilard,Training,2024-03-03,6:17:28,07:17:28,Training,Sunday +92,Billye Wenham,Marketing,2024-01-01,6:37:29,07:37:29,Meeting,Monday +93,Gisela Duley,Sales,2024-11-07,8:25:36,15:25:36,Meeting,Thursday +94,Iormina Grebner,Legal,2024-01-04,9:35:54,17:35:54,Other,Thursday +95,Bidget Sexton,Business Development,2024-09-05,8:00:48,15:00:48,Other,Thursday +96,Kerr Plet,Accounting,2024-02-06,9:51:24,15:51:24,Work,Tuesday +97,Sid Matton,Research and Development,2024-04-02,9:38:31,15:38:31,Other,Tuesday +98,Donia Cannavan,Support,2024-04-03,9:47:24,14:47:24,Work,Wednesday +99,Lyn Merring,Legal,2024-06-03,7:35:28,14:35:28,Training,Monday +100,Westleigh Glendza,Support,2024-10-01,9:32:27,12:32:27,Training,Tuesday +101,Vittoria Silliman,Legal,2024-01-02,8:21:22,14:21:22,Meeting,Tuesday +102,Constancia Henke,Legal,2024-08-03,9:59:16,11:59:16,Other,Saturday +103,Lorri Ferrieres,Human Resources,2024-11-01,6:32:07,08:32:07,Training,Friday +104,Layton Jansey,Accounting,2024-06-07,7:08:17,09:08:17,Meeting,Friday +105,Marjory O'Connel,Accounting,2024-05-01,7:50:05,10:50:05,Meeting,Wednesday +106,Roda Sketcher,Product Management,2024-07-01,7:55:29,10:55:29,Meeting,Monday +107,Florence Clay,Accounting,2024-12-04,9:32:20,16:32:20,Meeting,Wednesday +108,Delinda Goskar,Accounting,2024-01-05,6:36:17,13:36:17,Other,Friday +109,Brew Cluely,Training,2024-07-05,7:42:47,15:42:47,Meeting,Friday +110,Dougie Shilston,Research and Development,2024-04-04,8:39:19,10:39:19,Meeting,Thursday +111,Scotty O' Loughran,Marketing,2024-09-07,8:01:41,13:01:41,Other,Saturday +112,Juliann Starkings,Services,2024-09-02,6:51:44,14:51:44,Work,Monday +113,Maryl Caudle,Marketing,2024-10-07,6:14:35,10:14:35,Work,Monday +114,Helaina McDavid,Product Management,2024-04-01,8:07:19,13:07:19,Training,Monday +115,Cacilia Mayell,Accounting,2024-07-05,7:48:25,09:48:25,Work,Friday +116,Sean Garter,Training,2024-10-03,7:02:45,13:02:45,Meeting,Thursday +117,Lucilia Gantlett,Business Development,2024-03-03,9:06:02,11:06:02,Client Visit,Sunday +118,Joane Heindrick,Engineering,2024-06-07,9:57:05,10:57:05,Work,Friday +119,Pia Amsberger,Legal,2024-03-05,7:57:54,15:57:54,Work,Tuesday +120,Otha Hawker,Support,2024-05-07,7:59:48,15:59:48,Training,Tuesday +121,Polly Maxwaile,Business Development,2024-07-01,9:06:32,15:06:32,Work,Monday +122,Basil Bluett,Training,2024-09-03,7:08:16,12:08:16,Meeting,Tuesday +123,Korrie Cordet,Business Development,2024-07-05,6:24:21,14:24:21,Work,Friday +124,Essa Dymoke,Human Resources,2024-10-07,8:02:16,13:02:16,Other,Monday +125,Janos Rapa,Business Development,2024-12-05,8:55:17,12:55:17,Work,Thursday +126,Phedra Panner,Sales,2024-10-05,8:38:43,09:38:43,Meeting,Saturday +127,Shelley Ironside,Accounting,2024-12-06,7:53:02,12:53:02,Client Visit,Friday +128,Myrna Voak,Support,2024-01-05,7:07:27,09:07:27,Work,Friday +129,Rhianna Uttermare,Product Management,2024-06-05,9:46:01,14:46:01,Meeting,Wednesday +130,Julio Breffitt,Human Resources,2024-12-04,8:35:59,14:35:59,Training,Wednesday +131,Elijah Macklin,Engineering,2024-08-04,7:02:00,13:02:00,Training,Sunday +132,Alverta Schelle,Services,2024-04-03,9:57:00,17:57:00,Work,Wednesday +133,Melessa Peyton,Services,2024-05-05,7:40:24,10:40:24,Meeting,Sunday +134,Alwyn Gleadle,Research and Development,2024-11-04,6:33:04,09:33:04,Work,Monday +135,Archambault Larchiere,Services,2024-12-03,6:57:05,10:57:05,Meeting,Tuesday +136,Vern Sherrum,Legal,2024-06-07,7:58:30,13:58:30,Work,Friday +137,Avril Kohrs,Sales,2024-11-03,9:58:10,16:58:10,Meeting,Sunday +138,Crichton Spohrmann,Engineering,2024-03-03,7:08:25,10:08:25,Work,Sunday +139,Lila Firebrace,Product Management,2024-04-02,7:13:38,12:13:38,Meeting,Tuesday +140,Madge Prantoni,Support,2024-07-03,7:34:44,15:34:44,Meeting,Wednesday +141,Darla Towers,Sales,2024-11-03,7:13:57,13:13:57,Work,Sunday +142,Meaghan Schultheiss,Accounting,2024-02-05,6:56:00,11:56:00,Client Visit,Monday +143,Von Biddlestone,Product Management,2024-10-02,9:57:49,12:57:49,Client Visit,Wednesday +144,Zia Goodinge,Training,2024-08-03,6:17:31,11:17:31,Training,Saturday +145,Berta Grendon,Legal,2024-02-07,7:20:35,14:20:35,Other,Wednesday +146,Hobart Bison,Human Resources,2024-05-02,8:51:19,09:51:19,Meeting,Thursday +147,Sallyanne Daughtry,Product Management,2024-11-06,9:26:40,16:26:40,Meeting,Wednesday +148,Mae Unstead,Sales,2024-08-06,7:11:30,12:11:30,Work,Tuesday +149,Freddi Blare,Human Resources,2024-06-01,9:54:06,13:54:06,Work,Saturday +150,Sandye Pigden,Accounting,2024-01-01,9:49:58,11:49:58,Work,Monday +151,Abran Bozier,Business Development,2024-07-06,8:42:49,09:42:49,Work,Saturday +152,Ivan Querrard,Research and Development,2024-09-02,7:29:03,08:29:03,Meeting,Monday +153,Son McRill,Training,2024-01-07,9:44:47,10:44:47,Work,Sunday +154,Meridel Stutt,Services,2024-08-01,9:36:51,13:36:51,Work,Thursday +155,Delilah Spittles,Sales,2024-05-01,9:58:44,11:58:44,Client Visit,Wednesday +156,Avrom Pugsley,Human Resources,2024-10-07,7:22:14,14:22:14,Training,Monday +157,Faith Babbe,Business Development,2024-06-04,8:25:04,13:25:04,Work,Tuesday +158,Thorn Cranham,Marketing,2024-03-04,8:40:33,10:40:33,Client Visit,Monday +159,Ingeborg Vernazza,Marketing,2024-05-06,6:01:28,12:01:28,Client Visit,Monday +160,Jeri Greggersen,Engineering,2024-04-02,8:43:45,12:43:45,Other,Tuesday +161,Harwell Glasser,Support,2024-11-01,7:33:51,11:33:51,Client Visit,Friday +162,Clerc Fake,Product Management,2024-08-07,9:32:30,16:32:30,Meeting,Wednesday +163,Brent Dalgetty,Marketing,2024-07-07,7:39:35,15:39:35,Work,Sunday +164,Claudie Maryon,Marketing,2024-08-06,8:01:56,11:01:56,Client Visit,Tuesday +165,Benyamin Faragan,Accounting,2024-02-01,7:56:06,14:56:06,Work,Thursday +166,Ezmeralda Boissier,Training,2024-12-04,7:59:38,08:59:38,Meeting,Wednesday +167,Quintus Hedge,Training,2024-01-04,9:24:09,14:24:09,Meeting,Thursday +168,Antonius Bullimore,Engineering,2024-11-07,7:56:43,15:56:43,Work,Thursday +169,Tab de Werk,Legal,2024-01-07,9:24:58,10:24:58,Work,Sunday +170,Ofella Inseal,Business Development,2024-08-02,9:05:06,17:05:06,Meeting,Friday +171,Penn Hamshar,Services,2024-07-01,9:11:36,13:11:36,Work,Monday +172,Ricki Delagua,Research and Development,2024-11-06,7:28:22,10:28:22,Client Visit,Wednesday +173,Walton Woodburne,Support,2024-04-04,6:00:36,10:00:36,Meeting,Thursday +174,Jozef Rhelton,Services,2024-07-02,7:09:31,12:09:31,Client Visit,Tuesday +175,Sephira Cohan,Product Management,2024-01-01,8:45:53,09:45:53,Meeting,Monday +176,Abbey McCoughan,Training,2024-02-05,6:05:23,07:05:23,Meeting,Monday +177,Russell Petroulis,Marketing,2024-06-05,7:47:39,15:47:39,Meeting,Wednesday +178,Janek Ings,Research and Development,2024-10-05,7:20:17,15:20:17,Work,Saturday +179,Burnard Mullaly,Support,2024-06-06,7:47:11,13:47:11,Work,Thursday +180,Lilia Balcombe,Training,2024-11-05,6:41:24,07:41:24,Client Visit,Tuesday +181,Swen Iliff,Accounting,2024-07-06,6:38:16,13:38:16,Other,Saturday +182,Merrielle Woosnam,Business Development,2024-12-01,7:21:26,14:21:26,Client Visit,Sunday +183,Yorke Dinnies,Services,2024-10-06,8:46:22,09:46:22,Meeting,Sunday +184,Patsy Whannel,Accounting,2024-03-07,7:03:50,10:03:50,Meeting,Thursday +185,Leticia Krysztofiak,Legal,2024-02-04,8:43:23,09:43:23,Training,Sunday +186,Dael Tschierse,Marketing,2024-02-07,7:24:14,09:24:14,Work,Wednesday +187,Jojo Southwood,Services,2024-12-02,7:51:49,10:51:49,Work,Monday +188,Ferguson Frayling,Human Resources,2024-10-02,8:16:12,10:16:12,Training,Wednesday +189,Caryn Jurgensen,Marketing,2024-04-04,6:34:13,07:34:13,Meeting,Thursday +190,Maryellen Corran,Marketing,2024-05-01,6:58:08,07:58:08,Client Visit,Wednesday +191,Mufi Dhennin,Engineering,2024-05-05,7:36:34,09:36:34,Meeting,Sunday +192,Daven Heyball,Services,2024-04-05,9:32:33,12:32:33,Other,Friday +193,Addison Pentin,Human Resources,2024-07-06,9:43:38,16:43:38,Meeting,Saturday +194,Gideon Nockles,Services,2024-06-06,8:52:05,14:52:05,Work,Thursday +195,Ferdinande Fruchon,Accounting,2024-05-05,7:01:09,14:01:09,Work,Sunday +196,Appolonia Simyson,Research and Development,2024-10-04,9:01:54,14:01:54,Work,Friday +197,Sabra Wellington,Research and Development,2024-06-03,6:14:47,11:14:47,Meeting,Monday +198,Auroora Husselbee,Services,2024-10-07,8:42:20,15:42:20,Meeting,Monday +199,Charmian Pybus,Human Resources,2024-04-01,8:47:54,11:47:54,Work,Monday +200,Major Matuschek,Research and Development,2024-10-01,9:56:17,11:56:17,Work,Tuesday +201,Padraic Ginman,Legal,2024-10-02,8:22:13,16:22:13,Work,Wednesday +202,Martynne Brogiotti,Marketing,2024-10-01,6:34:24,10:34:24,Meeting,Tuesday +203,Ludwig Stede,Product Management,2024-10-01,8:09:07,09:09:07,Meeting,Tuesday +204,Skylar Maffioletti,Legal,2024-07-04,6:23:06,07:23:06,Work,Thursday +205,Babara Mazillius,Marketing,2024-11-02,8:06:37,09:06:37,Meeting,Saturday +206,Jamie Mulvihill,Legal,2024-12-02,6:49:00,09:49:00,Other,Monday +207,Philippe Yarranton,Services,2024-05-03,7:43:54,15:43:54,Meeting,Friday +208,Dur Eaglestone,Sales,2024-05-02,8:14:54,16:14:54,Meeting,Thursday +209,Linet Knowlden,Business Development,2024-01-06,8:03:12,10:03:12,Meeting,Saturday +210,Wilton Foskin,Research and Development,2024-09-05,6:34:19,08:34:19,Meeting,Thursday +211,Rhetta Dagnan,Training,2024-09-07,9:20:35,14:20:35,Other,Saturday +212,Konstance MacArd,Marketing,2024-03-01,8:54:45,16:54:45,Work,Friday +213,Hinda Ellgood,Sales,2024-03-04,7:32:02,12:32:02,Work,Monday +214,Ronny Iddins,Sales,2024-07-02,9:02:06,13:02:06,Meeting,Tuesday +215,Therese McQuode,Research and Development,2024-11-03,8:43:02,15:43:02,Client Visit,Sunday +216,Phoebe Slee,Business Development,2024-02-02,8:03:13,16:03:13,Work,Friday +217,Jade Wade,Support,2024-09-07,7:42:00,14:42:00,Training,Saturday +218,Christal Growcott,Research and Development,2024-04-05,7:40:12,13:40:12,Meeting,Friday +219,Aida Yurenin,Engineering,2024-03-03,6:22:34,10:22:34,Meeting,Sunday +220,Leopold Coutts,Human Resources,2024-10-05,6:07:17,11:07:17,Work,Saturday +221,Rockwell Farmery,Legal,2024-06-05,6:35:46,08:35:46,Other,Wednesday +222,Merill Kulvear,Business Development,2024-08-04,9:30:37,12:30:37,Meeting,Sunday +223,Lauryn Fish,Training,2024-02-05,7:01:32,08:01:32,Work,Monday +224,Licha Alesi,Research and Development,2024-08-04,7:43:21,14:43:21,Other,Sunday +225,Rice Cannings,Human Resources,2024-10-06,7:31:32,12:31:32,Work,Sunday +226,Elfrida Petrina,Research and Development,2024-04-02,7:38:57,11:38:57,Work,Tuesday +227,Kincaid Joselevitch,Engineering,2024-09-03,8:01:46,11:01:46,Meeting,Tuesday +228,Garfield Bagley,Support,2024-11-04,6:17:43,11:17:43,Work,Monday +229,Lidia Lesaunier,Sales,2024-02-05,6:50:11,13:50:11,Meeting,Monday +230,Marianna Bavridge,Training,2024-07-07,6:54:17,07:54:17,Work,Sunday +231,Quint Mullineux,Research and Development,2024-12-06,7:39:34,11:39:34,Work,Friday +232,Dominique Adcocks,Accounting,2024-09-01,6:07:30,07:07:30,Meeting,Sunday +233,Bordie Benoey,Training,2024-11-05,9:56:32,12:56:32,Work,Tuesday +234,Robinetta Crunkhurn,Human Resources,2024-09-05,8:08:38,15:08:38,Meeting,Thursday +235,Estel Rentoll,Business Development,2024-01-04,9:12:12,16:12:12,Other,Thursday +236,Aurie Wildbore,Research and Development,2024-08-07,6:43:53,14:43:53,Work,Wednesday +237,Morna Royston,Services,2024-03-07,8:49:30,14:49:30,Work,Thursday +238,Casper Flagg,Marketing,2024-10-01,6:31:41,10:31:41,Work,Tuesday +239,Jessee Frankland,Sales,2024-04-05,7:33:01,11:33:01,Work,Friday +240,Leighton Jirsa,Product Management,2024-03-01,9:11:04,10:11:04,Meeting,Friday +241,Gusella Gates,Business Development,2024-10-02,8:49:00,15:49:00,Meeting,Wednesday +242,Burke Dobie,Accounting,2024-04-06,7:38:47,13:38:47,Work,Saturday +243,Lizbeth Keme,Business Development,2024-03-04,7:41:54,08:41:54,Work,Monday +244,Correy Boow,Product Management,2024-04-02,9:48:11,17:48:11,Training,Tuesday +245,Arnie Yuryaev,Accounting,2024-06-02,7:08:00,09:08:00,Other,Sunday +246,Konstanze Spraggs,Marketing,2024-11-01,6:21:13,10:21:13,Other,Friday +247,Carine Linny,Product Management,2024-08-04,7:28:01,08:28:01,Meeting,Sunday +248,Erny Beauman,Engineering,2024-08-03,7:44:31,10:44:31,Meeting,Saturday +249,Nyssa Streatfeild,Research and Development,2024-02-05,9:00:51,17:00:51,Meeting,Monday +250,Meggie Blackborn,Human Resources,2024-08-02,9:19:20,17:19:20,Work,Friday +251,Rayner Avramovitz,Support,2024-10-01,7:00:46,08:00:46,Meeting,Tuesday +252,Alard Crannis,Services,2024-01-04,8:07:18,13:07:18,Work,Thursday +253,Zarah Damrel,Human Resources,2024-12-04,6:12:31,11:12:31,Client Visit,Wednesday +254,Corney Liggett,Research and Development,2024-08-02,8:22:11,13:22:11,Work,Friday +255,Agna Reddan,Human Resources,2024-05-01,9:48:46,15:48:46,Meeting,Wednesday +256,Jock Eisig,Legal,2024-11-06,9:40:36,15:40:36,Work,Wednesday +257,Mallory Winterton,Engineering,2024-08-02,9:35:11,11:35:11,Client Visit,Friday +258,Dollie Ferminger,Training,2024-08-04,8:46:16,09:46:16,Meeting,Sunday +259,Thedrick Jackman,Accounting,2024-08-04,8:39:38,14:39:38,Work,Sunday +260,Arlyne Goering,Training,2024-03-07,8:49:37,11:49:37,Client Visit,Thursday +261,Adelaide Behnecke,Engineering,2024-01-04,8:57:29,10:57:29,Work,Thursday +262,Lilith Scrafton,Sales,2024-03-02,8:13:25,15:13:25,Work,Saturday +263,Flo Ferrucci,Business Development,2024-11-03,7:34:21,08:34:21,Training,Sunday +264,Emalia Burnapp,Engineering,2024-08-02,6:55:30,12:55:30,Meeting,Friday +265,Amelia Lattey,Engineering,2024-10-03,9:18:51,13:18:51,Meeting,Thursday +266,Jilli Heasly,Marketing,2024-02-05,6:23:11,08:23:11,Client Visit,Monday +267,Addie Hutcheson,Product Management,2024-02-04,9:31:48,12:31:48,Training,Sunday +268,Lotti Camock,Engineering,2024-12-05,6:02:33,08:02:33,Work,Thursday +269,Susi Riglesford,Marketing,2024-09-05,7:17:01,09:17:01,Meeting,Thursday +270,Crista Oldaker,Legal,2024-01-05,8:43:52,16:43:52,Work,Friday +271,Ardenia Casewell,Training,2024-11-01,7:54:03,09:54:03,Other,Friday +272,Margareta Hamson,Product Management,2024-06-03,9:37:39,15:37:39,Training,Monday +273,Trudie Yeowell,Services,2024-01-01,8:26:58,11:26:58,Meeting,Monday +274,Susannah Flicker,Marketing,2024-07-05,7:40:54,12:40:54,Client Visit,Friday +275,Bondon Kinig,Marketing,2024-10-05,7:11:43,09:11:43,Meeting,Saturday +276,Rolph Mulran,Human Resources,2024-03-04,6:32:52,12:32:52,Meeting,Monday +277,Isaac Ebbin,Legal,2024-03-02,7:53:23,15:53:23,Client Visit,Saturday +278,Sheppard Perkin,Research and Development,2024-10-01,6:26:58,13:26:58,Meeting,Tuesday +279,Haven Wieprecht,Support,2024-09-04,7:01:18,14:01:18,Work,Wednesday +280,Eudora Menilove,Human Resources,2024-05-03,6:42:31,07:42:31,Work,Friday +281,Sabine Tofanini,Services,2024-07-06,8:16:17,09:16:17,Training,Saturday +282,Sidnee Stienton,Legal,2024-02-03,7:12:14,15:12:14,Meeting,Saturday +283,Cordy Caygill,Support,2024-03-04,8:30:23,16:30:23,Work,Monday +284,Michael Mourton,Product Management,2024-04-04,8:06:09,14:06:09,Other,Thursday +285,Anthia Kernermann,Product Management,2024-02-04,8:14:03,14:14:03,Work,Sunday +286,Malissia Boothroyd,Training,2024-12-06,7:01:15,10:01:15,Meeting,Friday +287,Vergil Potte,Services,2024-02-04,6:36:39,08:36:39,Meeting,Sunday +288,Alard Burrus,Services,2024-01-02,8:16:53,12:16:53,Work,Tuesday +289,Leanna Butterick,Engineering,2024-01-02,9:23:52,11:23:52,Meeting,Tuesday +290,Todd Franzonello,Training,2024-08-07,6:06:29,11:06:29,Training,Wednesday +291,Vinson Ghirigori,Human Resources,2024-08-01,6:10:26,14:10:26,Meeting,Thursday +292,Xylina McCafferky,Marketing,2024-12-04,8:51:46,15:51:46,Training,Wednesday +293,Jacquenette Andreev,Accounting,2024-10-07,8:09:13,15:09:13,Other,Monday +294,Tedie Kornilyev,Support,2024-09-02,7:54:32,10:54:32,Meeting,Monday +295,Zollie O'Kennavain,Legal,2024-04-07,8:41:32,10:41:32,Meeting,Sunday +296,Egor Freezor,Human Resources,2024-12-04,9:15:47,15:15:47,Training,Wednesday +297,Tommy Ebertz,Services,2024-03-07,7:42:55,12:42:55,Meeting,Thursday +298,Vilma Gilcrist,Training,2024-06-07,6:49:16,10:49:16,Meeting,Friday +299,Flynn Tort,Training,2024-07-03,6:41:40,13:41:40,Work,Wednesday +300,Ailey Titlow,Sales,2024-11-05,7:11:44,09:11:44,Training,Tuesday +301,Bradan Merriment,Training,2024-12-06,9:11:29,10:11:29,Meeting,Friday +302,Jobey Castagnone,Services,2024-05-06,8:14:28,09:14:28,Meeting,Monday +303,Baudoin Marrison,Services,2024-07-04,6:06:50,09:06:50,Meeting,Thursday +304,Chan Stell,Legal,2024-01-06,6:52:36,08:52:36,Client Visit,Saturday +305,Andy Dommersen,Product Management,2024-02-02,6:22:04,12:22:04,Work,Friday +306,Hester Cammell,Business Development,2024-02-06,7:07:05,15:07:05,Meeting,Tuesday +307,Lanita Learmount,Training,2024-07-07,8:25:31,12:25:31,Meeting,Sunday +308,Bink Moncreiff,Product Management,2024-01-07,6:49:35,13:49:35,Work,Sunday +309,Caryn Seamen,Training,2024-04-07,9:46:52,17:46:52,Meeting,Sunday +310,Valera Kelby,Marketing,2024-08-04,7:10:05,15:10:05,Meeting,Sunday +311,Jessa Wilcox,Engineering,2024-01-04,8:46:56,09:46:56,Work,Thursday +312,Shelley Edelmann,Accounting,2024-02-01,8:46:10,12:46:10,Meeting,Thursday +313,Alex Ginglell,Business Development,2024-05-05,9:50:52,10:50:52,Training,Sunday +314,Hakeem Emmison,Product Management,2024-02-02,7:23:01,15:23:01,Work,Friday +315,Sim Alesbrook,Research and Development,2024-04-05,9:52:06,13:52:06,Meeting,Friday +316,Nestor Reinisch,Accounting,2024-11-04,6:10:51,12:10:51,Client Visit,Monday +317,Brig Cordner,Product Management,2024-10-04,8:37:54,12:37:54,Training,Friday +318,John Siddons,Legal,2024-07-04,7:00:10,08:00:10,Meeting,Thursday +319,Mellie Jans,Marketing,2024-09-05,8:29:59,14:29:59,Work,Thursday +320,Zacharias Kures,Training,2024-02-07,6:44:55,08:44:55,Work,Wednesday +321,Kipper Driffill,Engineering,2024-06-07,7:36:29,12:36:29,Meeting,Friday +322,Davita Grinov,Marketing,2024-11-04,9:47:36,12:47:36,Meeting,Monday +323,Dorolice Arrol,Product Management,2024-04-05,7:27:00,12:27:00,Work,Friday +324,Guilbert Mynett,Sales,2024-09-01,7:33:23,15:33:23,Other,Sunday +325,Sibella Stivers,Product Management,2024-02-07,6:02:53,14:02:53,Meeting,Wednesday +326,Codie Housego,Accounting,2024-12-03,7:34:38,12:34:38,Client Visit,Tuesday +327,Isidoro Wadly,Business Development,2024-02-06,7:48:53,13:48:53,Work,Tuesday +328,Kelsey Kensington,Product Management,2024-05-06,6:42:16,11:42:16,Meeting,Monday +329,Niko Larciere,Product Management,2024-06-06,9:20:46,17:20:46,Meeting,Thursday +330,Eldredge Glastonbury,Sales,2024-11-03,7:14:58,13:14:58,Work,Sunday +331,Shae Wethey,Engineering,2024-08-02,6:16:10,11:16:10,Work,Friday +332,Nollie Isham,Training,2024-03-02,8:17:05,14:17:05,Work,Saturday +333,Filia Alden,Sales,2024-10-01,7:06:53,11:06:53,Work,Tuesday +334,Blakelee Erricker,Marketing,2024-10-07,9:10:46,13:10:46,Work,Monday +335,Grayce Sivell,Marketing,2024-07-04,7:44:17,10:44:17,Other,Thursday +336,Caria McBride,Marketing,2024-01-03,8:09:43,10:09:43,Meeting,Wednesday +337,Luisa Lymer,Sales,2024-06-05,9:10:34,12:10:34,Other,Wednesday +338,Murielle Adamiec,Accounting,2024-12-02,9:45:11,17:45:11,Meeting,Monday +339,Andreana Boswood,Sales,2024-07-01,8:13:15,12:13:15,Client Visit,Monday +340,Mersey Ambrose,Human Resources,2024-01-02,8:35:57,09:35:57,Meeting,Tuesday +341,Maury Ortells,Sales,2024-07-03,8:03:17,16:03:17,Work,Wednesday +342,Edward Piperley,Support,2024-10-07,7:35:49,15:35:49,Training,Monday +343,Abramo Clowney,Sales,2024-09-06,6:45:16,13:45:16,Training,Friday +344,Chandra Custed,Marketing,2024-11-04,9:02:26,10:02:26,Work,Monday +345,Maris Stolting,Human Resources,2024-07-04,6:54:49,12:54:49,Work,Thursday +346,Immanuel Davidov,Marketing,2024-07-07,6:23:24,11:23:24,Training,Sunday +347,Lanita Nenci,Sales,2024-12-05,8:38:35,09:38:35,Meeting,Thursday +348,Vina Lush,Sales,2024-03-04,9:21:51,10:21:51,Meeting,Monday +349,Florella Jans,Sales,2024-05-01,8:41:17,14:41:17,Training,Wednesday +350,Edmon de Vaen,Services,2024-12-05,8:58:04,14:58:04,Work,Thursday +351,Hailey Triggel,Research and Development,2024-02-02,9:43:10,15:43:10,Meeting,Friday +352,Bastian Kilsby,Training,2024-01-07,6:01:49,10:01:49,Work,Sunday +353,Care Hattam,Human Resources,2024-05-05,7:13:04,08:13:04,Meeting,Sunday +354,Lusa Easthope,Marketing,2024-08-03,8:39:00,09:39:00,Work,Saturday +355,Dionne Breen,Sales,2024-08-07,8:13:30,15:13:30,Meeting,Wednesday +356,Barbra Opfer,Product Management,2024-03-06,7:10:44,11:10:44,Other,Wednesday +357,Valentino Breem,Engineering,2024-01-05,8:51:02,16:51:02,Work,Friday +358,Bobbie Heinzel,Business Development,2024-07-02,6:42:52,09:42:52,Meeting,Tuesday +359,Kacey Bartolomucci,Accounting,2024-02-04,7:41:15,14:41:15,Client Visit,Sunday +360,Jorie Dibbe,Engineering,2024-05-01,6:44:32,08:44:32,Client Visit,Wednesday +361,Virgilio Axford,Accounting,2024-09-07,8:33:19,13:33:19,Work,Saturday +362,Benny Slograve,Marketing,2024-06-01,6:18:50,09:18:50,Training,Saturday +363,Augie Crunden,Services,2024-03-07,9:24:06,16:24:06,Work,Thursday +364,Sax Jennaroy,Legal,2024-03-03,6:45:26,09:45:26,Meeting,Sunday +365,Con Chopy,Services,2024-03-02,6:23:59,12:23:59,Meeting,Saturday +366,Corissa Oke,Accounting,2024-05-01,6:42:05,09:42:05,Client Visit,Wednesday +367,Andrei Brabon,Research and Development,2024-12-03,8:20:27,16:20:27,Meeting,Tuesday +368,Gabie Branwhite,Marketing,2024-09-07,9:30:07,16:30:07,Meeting,Saturday +369,Olive Cantua,Human Resources,2024-08-05,7:17:45,10:17:45,Client Visit,Monday +370,Briny Egre,Marketing,2024-12-05,8:08:26,12:08:26,Meeting,Thursday +371,Blanca Firpo,Support,2024-01-04,9:19:51,13:19:51,Client Visit,Thursday +372,Mora Porter,Services,2024-04-01,9:13:35,16:13:35,Work,Monday +373,Corina Matysik,Human Resources,2024-10-07,9:45:31,16:45:31,Client Visit,Monday +374,Aprilette Wakley,Engineering,2024-11-02,8:13:03,09:13:03,Training,Saturday +375,April McAllaster,Marketing,2024-03-06,9:27:19,16:27:19,Work,Wednesday +376,Gustavo Parken,Support,2024-11-07,7:22:23,08:22:23,Work,Thursday +377,Dylan Bucktharp,Product Management,2024-02-05,7:51:07,12:51:07,Work,Monday +378,Mabel Kenwrick,Sales,2024-03-06,7:06:46,14:06:46,Meeting,Wednesday +379,Tanitansy MacKerley,Accounting,2024-03-02,6:58:12,13:58:12,Client Visit,Saturday +380,Gustavo Sewards,Sales,2024-03-04,9:27:36,15:27:36,Meeting,Monday +381,Oswell Maunton,Business Development,2024-06-02,8:04:32,16:04:32,Training,Sunday +382,Rochette Choake,Training,2024-08-02,9:04:47,15:04:47,Work,Friday +383,George Saville,Legal,2024-03-02,9:20:08,14:20:08,Work,Saturday +384,Mariquilla Clem,Accounting,2024-03-01,9:15:53,16:15:53,Work,Friday +385,Courtnay Mulvaney,Engineering,2024-05-06,9:31:14,14:31:14,Meeting,Monday +386,Lanette Secombe,Legal,2024-02-01,8:39:12,11:39:12,Other,Thursday +387,Pace Morstatt,Accounting,2024-07-03,6:37:08,08:37:08,Meeting,Wednesday +388,Fons Alty,Training,2024-06-05,9:39:26,12:39:26,Meeting,Wednesday +389,Yehudit Lewington,Legal,2024-05-01,6:32:35,08:32:35,Other,Wednesday +390,Colline Barnaclough,Product Management,2024-05-06,8:58:53,10:58:53,Meeting,Monday +391,Korey Pooley,Legal,2024-09-07,9:57:24,11:57:24,Meeting,Saturday +392,Alex Hedney,Human Resources,2024-04-01,8:27:03,14:27:03,Work,Monday +393,Laura Chaize,Accounting,2024-10-05,6:14:52,08:14:52,Other,Saturday +394,Dorena Swatten,Accounting,2024-03-05,6:39:56,13:39:56,Work,Tuesday +395,Joleen McCutcheon,Research and Development,2024-02-07,8:50:40,15:50:40,Meeting,Wednesday +396,Adore Blair,Accounting,2024-10-03,7:25:27,15:25:27,Work,Thursday +397,Nancee Ritch,Legal,2024-10-06,9:59:19,10:59:19,Work,Sunday +398,Port Cockland,Research and Development,2024-06-04,8:16:44,13:16:44,Work,Tuesday +399,Alphonse Baker,Support,2024-06-02,7:26:06,14:26:06,Work,Sunday +400,Jephthah Chanders,Services,2024-09-07,9:15:36,12:15:36,Client Visit,Saturday +401,Leonard Isakson,Accounting,2024-09-07,6:49:28,10:49:28,Meeting,Saturday +402,Lonny Goldwater,Research and Development,2024-12-03,8:46:52,12:46:52,Meeting,Tuesday +403,Burg Lampett,Accounting,2024-07-03,8:34:24,13:34:24,Work,Wednesday +404,Lucian Boddice,Legal,2024-12-07,7:35:14,09:35:14,Work,Saturday +405,Corrianne Paffett,Human Resources,2024-01-04,8:34:19,11:34:19,Meeting,Thursday +406,Daron Sute,Services,2024-03-07,7:20:01,08:20:01,Client Visit,Thursday +407,Moll Odegaard,Human Resources,2024-11-06,8:37:04,15:37:04,Client Visit,Wednesday +408,Onida Elletson,Legal,2024-06-06,7:45:48,09:45:48,Other,Thursday +409,Denys Snailham,Support,2024-08-02,8:44:50,09:44:50,Training,Friday +410,Niki Liddington,Human Resources,2024-05-02,9:52:58,13:52:58,Work,Thursday +411,Tara Mattusevich,Engineering,2024-02-07,8:37:39,15:37:39,Other,Wednesday +412,Langsdon Sebrens,Engineering,2024-12-02,7:06:09,15:06:09,Training,Monday +413,Anatole Batter,Business Development,2024-10-05,8:42:00,14:42:00,Other,Saturday +414,Jarrod Symers,Services,2024-11-07,8:02:23,16:02:23,Other,Thursday +415,Shaughn Doore,Legal,2024-09-06,6:09:45,09:09:45,Meeting,Friday +416,Didi Sanbrook,Research and Development,2024-07-01,9:14:04,13:14:04,Meeting,Monday +417,Dotti Naisby,Research and Development,2024-05-03,8:09:35,12:09:35,Meeting,Friday +418,Vince Placstone,Research and Development,2024-01-04,8:39:16,14:39:16,Work,Thursday +419,Shirline Gallaway,Accounting,2024-06-06,7:59:24,08:59:24,Client Visit,Thursday +420,Elbert De Nisco,Legal,2024-09-01,6:58:25,11:58:25,Meeting,Sunday +421,Shurlocke Caulcutt,Product Management,2024-11-05,9:13:09,15:13:09,Work,Tuesday +422,Thatch Piddick,Research and Development,2024-01-06,7:06:43,08:06:43,Work,Saturday +423,Farrand Dillet,Legal,2024-10-06,6:39:38,08:39:38,Training,Sunday +424,Shayna Lepard,Training,2024-01-01,7:34:43,11:34:43,Meeting,Monday +425,Aloisia Stonham,Legal,2024-07-06,9:41:49,15:41:49,Work,Saturday +426,Noam Baldree,Research and Development,2024-03-05,8:30:53,16:30:53,Meeting,Tuesday +427,Langsdon Fibbitts,Support,2024-09-01,9:33:35,10:33:35,Work,Sunday +428,Murray Gummery,Sales,2024-08-02,6:37:35,12:37:35,Work,Friday +429,Worden Sans,Engineering,2024-02-02,8:54:25,14:54:25,Training,Friday +430,Leyla Branney,Marketing,2024-05-06,7:17:35,15:17:35,Meeting,Monday +431,Tamra Lindegard,Services,2024-08-03,8:17:27,15:17:27,Client Visit,Saturday +432,Lanita Darcey,Sales,2024-05-02,7:24:26,09:24:26,Other,Thursday +433,Elysia Boaler,Research and Development,2024-01-03,6:41:42,13:41:42,Meeting,Wednesday +434,Alecia Iacomini,Human Resources,2024-02-01,7:44:52,13:44:52,Work,Thursday +435,Erinn Cubuzzi,Engineering,2024-07-01,6:34:29,10:34:29,Work,Monday +436,Prince Grzelak,Sales,2024-04-07,9:25:39,13:25:39,Meeting,Sunday +437,Lurlene Struss,Research and Development,2024-03-04,9:16:55,13:16:55,Work,Monday +438,Michele Dominico,Services,2024-01-02,9:11:32,10:11:32,Meeting,Tuesday +439,Denyse Flowith,Sales,2024-07-02,9:19:49,17:19:49,Meeting,Tuesday +440,Lanie Hawsby,Business Development,2024-02-07,6:55:29,09:55:29,Meeting,Wednesday +441,Chadwick Clem,Marketing,2024-04-04,9:12:42,15:12:42,Meeting,Thursday +442,Florida Nicholas,Human Resources,2024-02-01,9:06:56,16:06:56,Work,Thursday +443,Darrin Faulconbridge,Research and Development,2024-02-06,7:52:48,11:52:48,Meeting,Tuesday +444,Pauletta Scholz,Human Resources,2024-12-05,6:15:38,09:15:38,Meeting,Thursday +445,Collete Sillitoe,Research and Development,2024-06-06,6:48:07,08:48:07,Meeting,Thursday +446,Thibaud MacGraith,Business Development,2024-09-04,8:06:40,09:06:40,Work,Wednesday +447,Payton McGennis,Sales,2024-11-03,7:12:54,09:12:54,Client Visit,Sunday +448,Bil Brixham,Engineering,2024-04-06,9:53:13,13:53:13,Meeting,Saturday +449,Elysha Brabham,Engineering,2024-07-01,6:16:41,10:16:41,Meeting,Monday +450,Ira Joynt,Research and Development,2024-10-02,9:59:03,16:59:03,Meeting,Wednesday +451,Garreth Nehls,Training,2024-05-07,6:38:34,09:38:34,Work,Tuesday +452,Olympia Furber,Legal,2024-03-04,9:51:38,13:51:38,Training,Monday +453,Keelia Giddings,Training,2024-06-02,7:42:50,10:42:50,Client Visit,Sunday +454,Erina Heggman,Engineering,2024-01-06,9:55:35,17:55:35,Training,Saturday +455,Georges Addy,Sales,2024-11-07,9:56:43,16:56:43,Meeting,Thursday +456,Willa Hudleston,Accounting,2024-09-06,8:35:24,10:35:24,Other,Friday +457,Marcelo Hunton,Sales,2024-12-07,7:12:49,09:12:49,Work,Saturday +458,Libbey Brosini,Services,2024-12-05,6:35:53,11:35:53,Work,Thursday +459,Danya Sisson,Human Resources,2024-05-02,6:30:08,12:30:08,Client Visit,Thursday +460,Ramon Gaythwaite,Research and Development,2024-07-02,9:12:04,17:12:04,Client Visit,Tuesday +461,Kayla Drummer,Human Resources,2024-11-07,7:04:49,13:04:49,Meeting,Thursday +462,Emelia Corhard,Engineering,2024-10-07,7:32:26,12:32:26,Client Visit,Monday +463,Eloisa Itzakovitz,Product Management,2024-03-05,8:39:10,15:39:10,Client Visit,Tuesday +464,Gauthier Ducker,Training,2024-08-07,8:29:39,15:29:39,Work,Wednesday +465,Sloan Syrett,Marketing,2024-10-06,6:40:18,08:40:18,Other,Sunday +466,Trudy Sherrock,Human Resources,2024-08-05,7:06:43,11:06:43,Other,Monday +467,Rivy Gwillyam,Support,2024-06-02,9:39:59,10:39:59,Meeting,Sunday +468,Brad Trenholm,Accounting,2024-06-02,7:07:58,09:07:58,Work,Sunday +469,Tudor Vallack,Human Resources,2024-05-06,9:31:09,15:31:09,Work,Monday +470,Alexio Dyment,Services,2024-03-01,9:00:54,13:00:54,Training,Friday +471,Tuesday Farres,Product Management,2024-03-05,7:16:03,11:16:03,Work,Tuesday +472,Evy Khan,Sales,2024-02-04,9:26:29,15:26:29,Other,Sunday +473,Andie Paudin,Research and Development,2024-01-02,7:00:23,09:00:23,Work,Tuesday +474,Christie Brute,Legal,2024-05-05,9:03:39,17:03:39,Training,Sunday +475,Pattie Dowtry,Training,2024-03-04,7:59:30,11:59:30,Training,Monday +476,Ida Moxsom,Legal,2024-01-06,9:12:27,17:12:27,Meeting,Saturday +477,Terza Keeltagh,Accounting,2024-10-06,7:49:00,10:49:00,Meeting,Sunday +478,Bruno Byway,Sales,2024-11-04,7:54:53,14:54:53,Work,Monday +479,Cirstoforo Attenbrough,Accounting,2024-06-05,6:26:53,08:26:53,Client Visit,Wednesday +480,Ludvig Sellick,Business Development,2024-06-04,8:06:54,15:06:54,Other,Tuesday +481,Culley Rickasse,Product Management,2024-03-03,7:16:49,09:16:49,Work,Sunday +482,Shelli Faccini,Engineering,2024-03-07,7:40:28,14:40:28,Work,Thursday +483,Morrie Delue,Accounting,2024-02-04,8:26:52,15:26:52,Other,Sunday +484,Christiana Romeril,Marketing,2024-07-05,9:54:38,10:54:38,Other,Friday +485,Jeremias Butland,Research and Development,2024-06-05,9:59:49,16:59:49,Meeting,Wednesday +486,Ennis Fowlie,Accounting,2024-10-03,6:57:56,10:57:56,Meeting,Thursday +487,Nissa Seagrove,Human Resources,2024-11-01,7:09:05,11:09:05,Work,Friday +488,Early Drissell,Marketing,2024-01-07,8:11:56,14:11:56,Work,Sunday +489,Cristobal Izkovicz,Legal,2024-05-01,9:27:28,13:27:28,Other,Wednesday +490,Ulric Geraldez,Services,2024-08-04,9:18:49,15:18:49,Other,Sunday +491,Katrinka Yitzhakov,Legal,2024-10-07,8:25:47,13:25:47,Other,Monday +492,Drona Mougeot,Accounting,2024-01-02,6:35:48,07:35:48,Training,Tuesday +493,Pamelina Grinstead,Business Development,2024-12-06,7:17:32,11:17:32,Meeting,Friday +494,Joey Hassen,Training,2024-10-06,9:24:41,16:24:41,Client Visit,Sunday +495,Karlotta Kettlestringe,Marketing,2024-04-07,8:19:06,11:19:06,Other,Sunday +496,Rick Espina,Accounting,2024-02-02,9:39:43,14:39:43,Meeting,Friday +497,Bamby Thexton,Training,2024-03-04,8:35:07,12:35:07,Work,Monday +498,Scott Vynoll,Accounting,2024-01-04,7:34:29,13:34:29,Meeting,Thursday +499,Whittaker Glidden,Research and Development,2024-02-07,8:43:54,11:43:54,Work,Wednesday +500,Sande Maffiotti,Support,2024-02-02,7:56:00,14:56:00,Other,Friday +501,Trefor Kettle,Human Resources,2024-02-02,9:42:47,17:42:47,Meeting,Friday +502,Andria Pele,Engineering,2024-11-01,9:58:58,12:58:58,Work,Friday +503,Virgilio Gioani,Sales,2024-03-04,9:09:33,16:09:33,Other,Monday +504,Roderigo Kield,Research and Development,2024-07-04,9:53:30,17:53:30,Meeting,Thursday +505,Jacklin Jacob,Services,2024-06-05,6:19:19,12:19:19,Meeting,Wednesday +506,Aluin Trammel,Training,2024-11-03,6:29:29,07:29:29,Other,Sunday +507,Athena Braham,Marketing,2024-12-03,9:02:18,14:02:18,Work,Tuesday +508,Kelila Pitkeathley,Business Development,2024-09-01,7:34:21,11:34:21,Meeting,Sunday +509,Randolph Ackery,Accounting,2024-08-03,7:37:14,13:37:14,Work,Saturday +510,Glyn Leftwich,Accounting,2024-12-04,9:58:37,17:58:37,Meeting,Wednesday +511,Letisha Moseley,Training,2024-07-04,7:13:39,08:13:39,Work,Thursday +512,Harwilll Dymott,Training,2024-05-01,8:21:35,15:21:35,Meeting,Wednesday +513,Karlyn Leap,Accounting,2024-05-01,7:02:57,13:02:57,Work,Wednesday +514,Cammy Texton,Support,2024-02-02,7:53:30,08:53:30,Meeting,Friday +515,Hilton Arntzen,Marketing,2024-12-03,8:22:15,15:22:15,Work,Tuesday +516,Ashla Render,Legal,2024-06-01,8:24:17,15:24:17,Work,Saturday +517,Mitzi Franciottoi,Product Management,2024-07-06,6:24:33,10:24:33,Work,Saturday +518,Courtnay Semmence,Training,2024-12-04,6:27:13,09:27:13,Client Visit,Wednesday +519,Ansell Petz,Business Development,2024-10-05,9:19:30,14:19:30,Work,Saturday +520,Archibold Bohan,Accounting,2024-08-03,8:13:11,15:13:11,Meeting,Saturday +521,Hercules Wedlock,Business Development,2024-07-05,9:27:29,11:27:29,Work,Friday +522,Debor Hecks,Engineering,2024-03-06,9:34:00,17:34:00,Training,Wednesday +523,Ernst Trythall,Business Development,2024-04-03,7:07:43,15:07:43,Client Visit,Wednesday +524,Dori Lyford,Human Resources,2024-03-02,7:47:41,10:47:41,Meeting,Saturday +525,Hart Capel,Business Development,2024-11-05,8:54:21,09:54:21,Work,Tuesday +526,Sax Burlay,Services,2024-03-05,7:07:50,09:07:50,Meeting,Tuesday +527,Elisha Lyvon,Support,2024-01-06,9:24:13,10:24:13,Work,Saturday +528,Maure Colleymore,Business Development,2024-08-06,7:57:48,10:57:48,Meeting,Tuesday +529,Tristam Divell,Marketing,2024-06-05,7:18:15,10:18:15,Meeting,Wednesday +530,Ignaz Howsego,Accounting,2024-06-06,7:56:10,08:56:10,Meeting,Thursday +531,Samuele Cromb,Research and Development,2024-05-07,8:28:18,15:28:18,Work,Tuesday +532,Sallyanne Brinklow,Support,2024-12-01,9:19:30,17:19:30,Training,Sunday +533,Petrina Carrabot,Human Resources,2024-03-01,9:56:56,14:56:56,Meeting,Friday +534,Arleta Beyer,Accounting,2024-06-07,8:13:07,15:13:07,Training,Friday +535,Denny Milthorpe,Legal,2024-09-04,8:35:17,11:35:17,Work,Wednesday +536,Gaylene Merck,Services,2024-02-07,7:02:09,09:02:09,Client Visit,Wednesday +537,Tremaine Reside,Research and Development,2024-08-07,6:21:15,11:21:15,Other,Wednesday +538,Lucinda Roddick,Human Resources,2024-04-05,9:13:45,12:13:45,Meeting,Friday +539,Chrisse Aberkirder,Training,2024-07-06,8:46:18,11:46:18,Client Visit,Saturday +540,Astrix Chesley,Support,2024-09-01,7:31:47,14:31:47,Other,Sunday +541,Peggi Dale,Accounting,2024-10-05,8:27:26,16:27:26,Meeting,Saturday +542,Joscelin Fidgett,Accounting,2024-06-03,9:52:55,13:52:55,Other,Monday +543,Guglielma Dedenham,Research and Development,2024-08-06,8:28:28,16:28:28,Meeting,Tuesday +544,Jecho Tyre,Sales,2024-07-04,7:26:44,09:26:44,Work,Thursday +545,Lynelle Brehault,Product Management,2024-10-01,6:44:00,08:44:00,Meeting,Tuesday +546,Paxon Mariner,Accounting,2024-07-02,7:12:15,14:12:15,Client Visit,Tuesday +547,Angil Darlison,Accounting,2024-11-03,9:45:15,15:45:15,Training,Sunday +548,Engelbert Szymczyk,Sales,2024-11-06,9:04:48,16:04:48,Other,Wednesday +549,Kristopher Rushmare,Business Development,2024-02-04,8:43:14,10:43:14,Work,Sunday +550,Danila Lidgey,Business Development,2024-07-01,7:08:57,08:08:57,Training,Monday +551,Hilliard Burlay,Human Resources,2024-11-06,9:07:44,15:07:44,Meeting,Wednesday +552,Merna Gillions,Accounting,2024-03-01,6:54:27,09:54:27,Other,Friday +553,Christalle White,Accounting,2024-07-07,7:33:04,08:33:04,Work,Sunday +554,Romola Hastwall,Services,2024-09-01,8:30:42,14:30:42,Client Visit,Sunday +555,Filbert Davison,Legal,2024-04-03,8:35:33,13:35:33,Meeting,Wednesday +556,Jacquelyn Odgaard,Legal,2024-05-02,6:42:03,12:42:03,Meeting,Thursday +557,Allys Langforth,Marketing,2024-03-02,8:38:47,16:38:47,Work,Saturday +558,Kettie Thursby,Legal,2024-03-03,9:16:57,12:16:57,Work,Sunday +559,Mimi Valentim,Engineering,2024-10-04,6:31:25,12:31:25,Training,Friday +560,Osborne Thunders,Support,2024-07-07,9:20:12,10:20:12,Client Visit,Sunday +561,Sherie Cuphus,Product Management,2024-10-05,6:21:49,08:21:49,Meeting,Saturday +562,Barri Jaulme,Human Resources,2024-04-03,8:25:10,15:25:10,Work,Wednesday +563,Alyson Sommersett,Training,2024-04-01,7:49:03,08:49:03,Other,Monday +564,Allison Greschik,Support,2024-08-01,8:35:39,13:35:39,Work,Thursday +565,Tomasina Polfer,Product Management,2024-07-02,9:44:35,12:44:35,Meeting,Tuesday +566,Gannon Camplen,Product Management,2024-01-04,7:10:58,15:10:58,Client Visit,Thursday +567,Stavro Swadlinge,Human Resources,2024-09-02,7:58:55,12:58:55,Work,Monday +568,Lavinia Aistrop,Support,2024-09-03,6:49:09,13:49:09,Work,Tuesday +569,Trixi Mealing,Human Resources,2024-03-07,6:54:29,14:54:29,Other,Thursday +570,Ralph Bleier,Support,2024-12-03,7:23:06,09:23:06,Other,Tuesday +571,Babita McEniry,Human Resources,2024-06-05,9:57:09,15:57:09,Work,Wednesday +572,Stanislaw Forcer,Accounting,2024-07-05,7:46:01,13:46:01,Other,Friday +573,Delora Macvey,Business Development,2024-01-01,6:47:38,14:47:38,Training,Monday +574,Matthus Faucett,Support,2024-08-06,8:38:55,15:38:55,Work,Tuesday +575,Stesha Pardal,Accounting,2024-09-01,6:41:03,07:41:03,Client Visit,Sunday +576,Phebe Mar,Research and Development,2024-10-02,6:40:10,07:40:10,Meeting,Wednesday +577,Etta Danielsen,Marketing,2024-01-01,7:14:01,08:14:01,Meeting,Monday +578,Lyndsie Brenton,Business Development,2024-11-04,8:04:50,09:04:50,Other,Monday +579,Haywood Muller,Support,2024-03-07,8:20:38,14:20:38,Work,Thursday +580,Winslow Minogue,Business Development,2024-02-01,8:04:19,14:04:19,Other,Thursday +581,Barri Sandbatch,Product Management,2024-08-03,9:45:37,17:45:37,Other,Saturday +582,Dagny Romke,Accounting,2024-08-03,9:59:41,16:59:41,Work,Saturday +583,Jeffie Benko,Legal,2024-06-03,8:39:43,11:39:43,Client Visit,Monday +584,Krystle Episcopi,Research and Development,2024-04-07,9:19:40,11:19:40,Other,Sunday +585,Shane Kmicicki,Research and Development,2024-03-01,6:30:18,09:30:18,Work,Friday +586,Missie Eberle,Training,2024-10-04,7:50:48,08:50:48,Work,Friday +587,Danit Rhead,Engineering,2024-05-07,7:04:48,09:04:48,Meeting,Tuesday +588,Percy Thunder,Engineering,2024-09-03,8:53:50,16:53:50,Work,Tuesday +589,Padget Arias,Human Resources,2024-11-06,6:25:21,07:25:21,Meeting,Wednesday +590,Lowrance Blewitt,Product Management,2024-11-03,6:43:26,08:43:26,Meeting,Sunday +591,Susanne Marlow,Research and Development,2024-09-01,8:09:19,12:09:19,Meeting,Sunday +592,Paolo Caffrey,Marketing,2024-04-01,6:54:25,07:54:25,Work,Monday +593,Cos Tonkin,Legal,2024-07-07,9:20:14,17:20:14,Client Visit,Sunday +594,Arabelle Brearty,Business Development,2024-01-01,6:46:08,10:46:08,Meeting,Monday +595,Teddi Klyn,Services,2024-05-04,7:48:00,09:48:00,Training,Saturday +596,Stanwood Westmancoat,Marketing,2024-08-03,8:36:12,13:36:12,Other,Saturday +597,Ambur Palliser,Sales,2024-08-04,6:37:29,13:37:29,Meeting,Sunday +598,Theresita Pinchbeck,Sales,2024-03-01,6:57:06,07:57:06,Meeting,Friday +599,Kayle Sprionghall,Services,2024-06-07,9:39:04,14:39:04,Meeting,Friday +600,Percy Blunden,Accounting,2024-03-03,6:51:56,13:51:56,Work,Sunday +601,Tyler Itzhaiek,Sales,2024-03-06,6:16:50,14:16:50,Meeting,Wednesday +602,Arleyne Jonah,Marketing,2024-07-06,9:47:02,15:47:02,Work,Saturday +603,Frederico McLeish,Marketing,2024-10-01,7:34:22,15:34:22,Work,Tuesday +604,Aloin Klassmann,Accounting,2024-06-04,8:35:58,16:35:58,Work,Tuesday +605,Lorne Hawtrey,Business Development,2024-08-02,7:10:04,15:10:04,Meeting,Friday +606,Hogan Brahm,Human Resources,2024-04-07,8:51:40,13:51:40,Work,Sunday +607,Shauna McFeat,Product Management,2024-08-06,8:33:25,12:33:25,Work,Tuesday +608,Meade Curro,Accounting,2024-09-01,9:47:35,16:47:35,Work,Sunday +609,Pollyanna Thornley,Sales,2024-08-03,9:40:54,11:40:54,Work,Saturday +610,Shanon Nawton,Marketing,2024-06-03,9:18:14,16:18:14,Work,Monday +611,Amalia Lantaph,Accounting,2024-07-05,7:49:17,14:49:17,Meeting,Friday +612,Barry Gallamore,Services,2024-01-04,6:50:44,13:50:44,Training,Thursday +613,Sib Tomaino,Accounting,2024-03-07,6:25:40,12:25:40,Client Visit,Thursday +614,Alli Tremoille,Product Management,2024-11-05,9:54:20,16:54:20,Work,Tuesday +615,Tybie Vossing,Marketing,2024-10-07,8:36:36,09:36:36,Meeting,Monday +616,Jocko Liddy,Research and Development,2024-06-07,7:31:17,13:31:17,Training,Friday +617,Cristy Labb,Legal,2024-01-03,9:26:59,17:26:59,Meeting,Wednesday +618,Mathilda Merrall,Research and Development,2024-01-07,8:27:42,12:27:42,Meeting,Sunday +619,Kyle Boffin,Engineering,2024-09-07,6:10:16,09:10:16,Client Visit,Saturday +620,Zane Joiner,Product Management,2024-09-03,8:48:39,16:48:39,Work,Tuesday +621,Em Lorryman,Engineering,2024-02-02,8:24:59,09:24:59,Meeting,Friday +622,Elliott Lombard,Services,2024-09-07,6:50:55,08:50:55,Work,Saturday +623,Finn Hatz,Sales,2024-11-07,7:37:19,11:37:19,Meeting,Thursday +624,Jamil MacCartan,Support,2024-08-04,7:20:30,13:20:30,Other,Sunday +625,Kylen Brocks,Marketing,2024-01-02,9:33:34,12:33:34,Work,Tuesday +626,Francisca Scollick,Sales,2024-02-02,9:24:52,15:24:52,Meeting,Friday +627,Inness Van Son,Research and Development,2024-10-02,8:00:32,09:00:32,Meeting,Wednesday +628,Trish Rodmell,Accounting,2024-03-02,7:12:00,13:12:00,Work,Saturday +629,Lauren Bowstead,Business Development,2024-09-07,7:17:06,14:17:06,Client Visit,Saturday +630,Tarah Dilnot,Legal,2024-09-06,7:12:52,15:12:52,Training,Friday +631,Dallas Simion,Accounting,2024-07-04,9:40:30,15:40:30,Work,Thursday +632,Leontine Sutherley,Accounting,2024-04-06,7:04:15,11:04:15,Meeting,Saturday +633,Kirsten Erington,Sales,2024-11-04,8:01:07,11:01:07,Meeting,Monday +634,Gerri Beebis,Human Resources,2024-10-05,7:10:15,10:10:15,Other,Saturday +635,Zorana Ballinger,Marketing,2024-04-03,6:18:58,12:18:58,Work,Wednesday +636,Shep Backhurst,Engineering,2024-07-03,9:03:56,10:03:56,Work,Wednesday +637,Darelle Sallery,Accounting,2024-03-04,9:20:29,17:20:29,Client Visit,Monday +638,Gualterio Neles,Support,2024-05-07,6:34:35,07:34:35,Work,Tuesday +639,Hussein Lintott,Research and Development,2024-02-07,8:14:56,09:14:56,Other,Wednesday +640,Ken Staterfield,Engineering,2024-08-06,6:15:35,14:15:35,Meeting,Tuesday +641,Ashely Beccero,Business Development,2024-10-01,6:01:42,09:01:42,Work,Tuesday +642,Ezmeralda Baxstar,Accounting,2024-08-05,9:06:45,14:06:45,Meeting,Monday +643,Berrie Meardon,Engineering,2024-06-03,6:29:35,14:29:35,Work,Monday +644,Octavia Lishman,Sales,2024-07-04,7:32:12,14:32:12,Client Visit,Thursday +645,Kendricks Aikett,Business Development,2024-11-04,8:31:19,13:31:19,Work,Monday +646,Daisi Jupe,Marketing,2024-09-07,6:01:23,09:01:23,Work,Saturday +647,Ali Bulger,Product Management,2024-05-06,6:39:35,08:39:35,Client Visit,Monday +648,Ikey Topper,Sales,2024-12-01,8:32:17,10:32:17,Training,Sunday +649,Herc McGlade,Sales,2024-07-01,9:03:35,13:03:35,Client Visit,Monday +650,Jeremias Legon,Engineering,2024-07-01,9:56:06,16:56:06,Training,Monday +651,Natala Iffland,Marketing,2024-08-07,6:55:53,09:55:53,Work,Wednesday +652,Stevie Bamborough,Accounting,2024-06-02,6:12:34,08:12:34,Training,Sunday +653,Mata Davidesco,Services,2024-07-01,7:19:50,13:19:50,Work,Monday +654,Fraser Hedon,Sales,2024-03-01,8:01:00,14:01:00,Meeting,Friday +655,Barnebas Boleyn,Support,2024-08-04,7:00:24,15:00:24,Work,Sunday +656,Olympia Blunsen,Engineering,2024-12-04,8:48:08,11:48:08,Client Visit,Wednesday +657,Laureen Gleeson,Training,2024-11-01,7:08:55,11:08:55,Other,Friday +658,Giana Beadnell,Business Development,2024-12-02,6:05:53,12:05:53,Meeting,Monday +659,Bruis Buxsy,Training,2024-01-06,8:06:03,16:06:03,Work,Saturday +660,Shelley Medcalf,Product Management,2024-03-06,6:13:52,11:13:52,Meeting,Wednesday +661,Markus Garton,Business Development,2024-12-04,9:21:51,10:21:51,Client Visit,Wednesday +662,Risa Rosel,Legal,2024-08-04,7:01:15,08:01:15,Meeting,Sunday +663,Konstanze Northey,Research and Development,2024-10-03,7:43:47,14:43:47,Client Visit,Thursday +664,Maude Pochet,Human Resources,2024-12-03,8:48:08,11:48:08,Meeting,Tuesday +665,Mead Gerdts,Services,2024-09-06,6:19:46,14:19:46,Work,Friday +666,Carroll Ransley,Training,2024-06-05,6:34:03,08:34:03,Training,Wednesday +667,Lucky De Zuani,Business Development,2024-09-04,9:39:25,13:39:25,Meeting,Wednesday +668,Cyndia Croote,Support,2024-04-01,6:20:21,12:20:21,Work,Monday +669,Finn Vignaux,Legal,2024-12-03,8:40:48,16:40:48,Meeting,Tuesday +670,Katy Barthod,Accounting,2024-05-06,9:54:29,14:54:29,Meeting,Monday +671,Hieronymus Senechault,Product Management,2024-06-07,6:01:11,14:01:11,Meeting,Friday +672,Maddi Service,Engineering,2024-04-03,9:45:23,14:45:23,Meeting,Wednesday +673,Archambault Grim,Marketing,2024-03-06,6:07:20,10:07:20,Client Visit,Wednesday +674,Washington Brokenbrow,Training,2024-05-01,7:45:16,10:45:16,Work,Wednesday +675,Haze Kubelka,Support,2024-09-04,9:53:47,15:53:47,Meeting,Wednesday +676,Clem Kristufek,Sales,2024-06-04,8:47:31,09:47:31,Training,Tuesday +677,Jobyna Mathison,Support,2024-08-04,9:39:58,11:39:58,Meeting,Sunday +678,Marthena Gawke,Legal,2024-06-04,9:44:18,12:44:18,Meeting,Tuesday +679,Gail Sutherns,Engineering,2024-09-04,6:46:46,08:46:46,Meeting,Wednesday +680,Valene Middlehurst,Product Management,2024-03-03,7:06:58,10:06:58,Work,Sunday +681,Candide Berling,Product Management,2024-01-06,7:58:08,15:58:08,Meeting,Saturday +682,Darrell Bugdale,Support,2024-11-07,7:48:30,10:48:30,Meeting,Thursday +683,Elvis Schops,Support,2024-12-07,6:51:56,12:51:56,Meeting,Saturday +684,Simonne Spiniello,Support,2024-05-07,8:42:29,16:42:29,Work,Tuesday +685,Quintin Fidgett,Human Resources,2024-08-02,7:51:50,14:51:50,Client Visit,Friday +686,Ruddy Ambrogelli,Business Development,2024-12-06,7:02:13,11:02:13,Client Visit,Friday +687,Towney Melody,Product Management,2024-06-05,6:55:14,10:55:14,Client Visit,Wednesday +688,Vlad Weddeburn - Scrimgeour,Human Resources,2024-04-05,7:28:42,13:28:42,Client Visit,Friday +689,Alla Whodcoat,Sales,2024-01-05,8:30:12,13:30:12,Training,Friday +690,Walker Onele,Engineering,2024-09-07,8:29:55,16:29:55,Client Visit,Saturday +691,Fifi Joly,Research and Development,2024-02-07,6:12:57,11:12:57,Other,Wednesday +692,Osborn Showler,Business Development,2024-02-02,9:50:25,14:50:25,Meeting,Friday +693,Joachim Hands,Human Resources,2024-09-03,8:54:33,15:54:33,Client Visit,Tuesday +694,Harland Rathbone,Training,2024-04-01,7:34:20,11:34:20,Other,Monday +695,Augustina McFeate,Human Resources,2024-01-03,8:30:07,16:30:07,Other,Wednesday +696,Charyl Prest,Marketing,2024-03-05,7:23:18,09:23:18,Meeting,Tuesday +697,Ilsa Berthouloume,Product Management,2024-01-05,8:57:30,14:57:30,Work,Friday +698,Issie Joslyn,Human Resources,2024-04-03,9:56:04,15:56:04,Training,Wednesday +699,Stephi Eason,Product Management,2024-06-02,7:53:36,10:53:36,Meeting,Sunday +700,Dagny Standley,Human Resources,2024-10-06,9:43:42,16:43:42,Meeting,Sunday +701,Alphard Mellish,Business Development,2024-10-04,8:10:34,10:10:34,Client Visit,Friday +702,Falkner Askam,Product Management,2024-01-07,7:42:15,12:42:15,Meeting,Sunday +703,Kaja Cavan,Support,2024-08-07,6:37:22,07:37:22,Work,Wednesday +704,Joey Walpole,Business Development,2024-11-06,7:11:50,15:11:50,Client Visit,Wednesday +705,Ula Janosevic,Engineering,2024-12-01,6:22:18,10:22:18,Meeting,Sunday +706,Hyatt Tombleson,Training,2024-10-07,9:01:13,12:01:13,Meeting,Monday +707,Bruno Scamerden,Human Resources,2024-10-06,9:31:08,16:31:08,Work,Sunday +708,Zeb Paulsson,Sales,2024-06-04,6:35:11,12:35:11,Meeting,Tuesday +709,Artemus Norcliffe,Business Development,2024-04-01,6:19:30,09:19:30,Work,Monday +710,Antonino Feeney,Support,2024-08-05,6:17:16,12:17:16,Other,Monday +711,Jess Rigglesford,Human Resources,2024-05-03,8:52:22,12:52:22,Client Visit,Friday +712,Ben Smithson,Training,2024-10-02,7:39:18,08:39:18,Meeting,Wednesday +713,Gustavus Gallone,Support,2024-10-03,7:41:11,08:41:11,Work,Thursday +714,Gwennie Rudledge,Legal,2024-02-05,6:37:21,12:37:21,Training,Monday +715,Sigfrid Summerly,Support,2024-03-06,7:30:13,10:30:13,Training,Wednesday +716,Aurelia Grogor,Legal,2024-09-06,9:35:43,12:35:43,Meeting,Friday +717,Bekki Harkins,Legal,2024-12-02,9:20:53,12:20:53,Work,Monday +718,Domini Warn,Accounting,2024-05-04,7:38:31,14:38:31,Meeting,Saturday +719,Esdras Hawke,Support,2024-11-06,6:16:04,14:16:04,Meeting,Wednesday +720,Eulalie Toffel,Training,2024-06-03,8:55:49,10:55:49,Other,Monday +721,Paxton Studman,Legal,2024-04-06,6:57:54,10:57:54,Meeting,Saturday +722,Jaymie Fleeman,Research and Development,2024-10-05,8:36:23,14:36:23,Meeting,Saturday +723,Melita Sneesby,Sales,2024-03-05,7:11:51,11:11:51,Other,Tuesday +724,Gearalt Shillom,Support,2024-06-01,9:48:31,11:48:31,Work,Saturday +725,Nicolina Piatti,Training,2024-09-03,9:03:01,12:03:01,Meeting,Tuesday +726,Mathilda Moorhouse,Legal,2024-09-02,6:32:07,08:32:07,Other,Monday +727,Davidson Witch,Legal,2024-07-06,9:33:39,17:33:39,Meeting,Saturday +728,Barbee Limb,Product Management,2024-11-05,9:57:16,17:57:16,Training,Tuesday +729,Helsa Toxell,Services,2024-08-05,8:51:17,10:51:17,Work,Monday +730,Anabelle Ivy,Support,2024-10-07,9:37:12,15:37:12,Training,Monday +731,Kellsie Poyntz,Accounting,2024-09-06,7:17:20,10:17:20,Meeting,Friday +732,Stephen Bateman,Services,2024-08-03,6:23:30,10:23:30,Work,Saturday +733,Dayna Tine,Engineering,2024-08-02,7:11:23,12:11:23,Other,Friday +734,Josee Zecchinelli,Legal,2024-03-06,7:29:55,11:29:55,Work,Wednesday +735,Margot Nendick,Business Development,2024-07-07,6:53:25,12:53:25,Work,Sunday +736,Thaine Saddler,Product Management,2024-04-03,9:25:28,11:25:28,Meeting,Wednesday +737,Dewie Regenhardt,Research and Development,2024-06-04,9:58:48,16:58:48,Meeting,Tuesday +738,Donnell Teasdale-Markie,Business Development,2024-09-06,7:40:54,15:40:54,Meeting,Friday +739,Maurie Hubbock,Human Resources,2024-08-04,6:42:36,08:42:36,Meeting,Sunday +740,Yule Yerborn,Human Resources,2024-01-01,9:24:11,17:24:11,Work,Monday +741,Dulcine McLarens,Training,2024-10-06,9:26:17,13:26:17,Training,Sunday +742,Jillene Gibson,Engineering,2024-11-03,8:41:39,10:41:39,Meeting,Sunday +743,Ingra Jessep,Business Development,2024-10-07,9:16:28,14:16:28,Work,Monday +744,Pen Saby,Legal,2024-02-01,8:17:51,14:17:51,Training,Thursday +745,Angelico Tchir,Business Development,2024-08-03,7:54:24,15:54:24,Training,Saturday +746,Michal Consadine,Product Management,2024-07-04,8:52:23,12:52:23,Work,Thursday +747,Whitman McGurgan,Human Resources,2024-04-07,9:09:29,14:09:29,Meeting,Sunday +748,Allys Kielty,Sales,2024-01-02,7:33:33,09:33:33,Meeting,Tuesday +749,Ibbie Clampton,Accounting,2024-10-03,7:43:37,08:43:37,Meeting,Thursday +750,Dorothea Drinkwater,Training,2024-04-07,7:19:18,11:19:18,Other,Sunday +751,Rurik Feetham,Engineering,2024-06-03,6:46:29,10:46:29,Meeting,Monday +752,Mandie Renvoys,Research and Development,2024-05-03,6:06:22,13:06:22,Work,Friday +753,Manolo Fidian,Research and Development,2024-05-03,9:40:08,13:40:08,Work,Friday +754,Farrah Luscombe,Support,2024-08-03,9:22:59,13:22:59,Meeting,Saturday +755,Nolan Finessy,Product Management,2024-12-04,9:22:07,13:22:07,Work,Wednesday +756,Marita Leyband,Services,2024-05-07,8:50:42,13:50:42,Other,Tuesday +757,Frederigo Sarson,Services,2024-05-05,8:54:25,14:54:25,Work,Sunday +758,Pail Cessford,Sales,2024-12-05,6:36:17,08:36:17,Other,Thursday +759,Valentine Colchett,Human Resources,2024-08-03,8:18:30,10:18:30,Meeting,Saturday +760,Stevy Caulcutt,Marketing,2024-05-07,8:19:44,12:19:44,Other,Tuesday +761,Eveline Sears,Product Management,2024-05-01,7:00:04,15:00:04,Work,Wednesday +762,Kara Antonescu,Legal,2024-03-07,6:21:03,08:21:03,Other,Thursday +763,Cassy Hellard,Business Development,2024-05-03,6:24:17,12:24:17,Other,Friday +764,Erna Osbiston,Accounting,2024-05-05,6:35:07,14:35:07,Other,Sunday +765,Townie Hofton,Training,2024-05-06,6:41:43,13:41:43,Training,Monday +766,Earvin Dorling,Product Management,2024-08-02,6:42:00,12:42:00,Meeting,Friday +767,Sebastian Sambidge,Business Development,2024-12-06,8:01:25,11:01:25,Meeting,Friday +768,Georgy Kedward,Product Management,2024-09-03,7:33:08,15:33:08,Work,Tuesday +769,Maura Copsey,Accounting,2024-09-06,8:31:10,11:31:10,Meeting,Friday +770,Abagael Konzelmann,Product Management,2024-11-04,8:45:22,13:45:22,Training,Monday +771,Marysa Bosdet,Human Resources,2024-09-05,7:26:22,14:26:22,Meeting,Thursday +772,Stanfield Duham,Accounting,2024-12-03,6:13:12,12:13:12,Work,Tuesday +773,Morley Whittles,Accounting,2024-09-02,8:26:41,09:26:41,Work,Monday +774,Quinton Crombie,Engineering,2024-06-03,9:55:04,11:55:04,Meeting,Monday +775,Rosamund McTrustam,Sales,2024-03-02,9:59:21,16:59:21,Training,Saturday +776,Jonie Hellwig,Support,2024-10-04,6:37:10,11:37:10,Other,Friday +777,Fidelio Caw,Support,2024-05-05,6:52:33,11:52:33,Work,Sunday +778,Ralf Free,Business Development,2024-02-06,6:37:07,10:37:07,Meeting,Tuesday +779,Lorine Tothacot,Training,2024-10-04,7:50:09,11:50:09,Meeting,Friday +780,Aileen Millier,Legal,2024-06-07,7:39:59,13:39:59,Meeting,Friday +781,Cesar Forten,Legal,2024-01-05,8:09:44,13:09:44,Work,Friday +782,Wendie Summers,Marketing,2024-10-01,6:29:06,10:29:06,Training,Tuesday +783,Demeter Wallhead,Services,2024-09-04,6:54:19,11:54:19,Meeting,Wednesday +784,Anatol Edger,Research and Development,2024-10-06,7:57:32,13:57:32,Work,Sunday +785,Nicol Sonnenschein,Services,2024-08-05,8:19:16,12:19:16,Client Visit,Monday +786,Rufe Christensen,Product Management,2024-03-07,9:49:26,14:49:26,Meeting,Thursday +787,Cammy Twinterman,Product Management,2024-05-01,8:34:58,13:34:58,Meeting,Wednesday +788,Eyde Maddox,Support,2024-04-04,7:24:13,14:24:13,Meeting,Thursday +789,Jan Litherborough,Engineering,2024-02-07,8:53:37,14:53:37,Work,Wednesday +790,Sanderson Worster,Accounting,2024-07-06,7:20:22,08:20:22,Training,Saturday +791,Winna Abele,Human Resources,2024-07-02,8:18:28,16:18:28,Client Visit,Tuesday +792,Norry Foresight,Accounting,2024-12-05,7:03:52,13:03:52,Meeting,Thursday +793,Tobey Tomalin,Product Management,2024-10-06,6:54:49,12:54:49,Meeting,Sunday +794,Anastassia Bentje,Human Resources,2024-01-06,7:13:49,13:13:49,Work,Saturday +795,Chanda Kleinhausen,Marketing,2024-11-06,9:39:14,11:39:14,Other,Wednesday +796,Connor Dallison,Support,2024-10-03,8:13:17,10:13:17,Work,Thursday +797,Giles Ley,Business Development,2024-11-05,8:05:54,14:05:54,Meeting,Tuesday +798,Cele Bau,Legal,2024-08-01,6:06:51,08:06:51,Client Visit,Thursday +799,Loretta O'Corr,Research and Development,2024-05-07,8:45:44,10:45:44,Work,Tuesday +800,Lanita Mullin,Human Resources,2024-07-01,8:25:18,14:25:18,Meeting,Monday +801,Lyda Maylin,Research and Development,2024-02-02,7:19:45,10:19:45,Work,Friday +802,Lamond Dilloway,Human Resources,2024-08-05,6:34:16,07:34:16,Work,Monday +803,Thatcher Hazzard,Business Development,2024-02-06,7:29:45,13:29:45,Meeting,Tuesday +804,Merlina Pilling,Legal,2024-04-05,9:26:46,16:26:46,Work,Friday +805,Deanna Snaith,Accounting,2024-11-02,6:14:49,08:14:49,Meeting,Saturday +806,Ainslie Carrick,Services,2024-09-03,9:39:50,11:39:50,Meeting,Tuesday +807,Roderic Edel,Product Management,2024-12-05,9:47:11,17:47:11,Other,Thursday +808,Regan Tingle,Research and Development,2024-05-06,6:32:09,11:32:09,Other,Monday +809,Ripley Duesbury,Training,2024-11-07,6:59:55,09:59:55,Work,Thursday +810,Coralie Moulder,Product Management,2024-03-04,7:55:14,10:55:14,Client Visit,Monday +811,Farica Scarrott,Accounting,2024-01-04,9:27:00,16:27:00,Meeting,Thursday +812,Nanon Mabbott,Product Management,2024-05-04,8:10:56,12:10:56,Meeting,Saturday +813,Rebecka Salmoni,Sales,2024-10-02,7:04:49,14:04:49,Meeting,Wednesday +814,Linette Toogood,Services,2024-11-03,9:49:37,10:49:37,Meeting,Sunday +815,Alix Burnapp,Marketing,2024-02-02,8:08:09,11:08:09,Meeting,Friday +816,Elvira Spat,Support,2024-06-01,8:18:59,14:18:59,Client Visit,Saturday +817,Lock Enoksson,Marketing,2024-11-01,8:40:41,14:40:41,Work,Friday +818,Reece Winders,Research and Development,2024-11-03,8:41:31,11:41:31,Meeting,Sunday +819,Domenic Phelan,Business Development,2024-04-07,9:55:31,14:55:31,Client Visit,Sunday +820,Tracy Beedon,Product Management,2024-10-02,9:20:45,12:20:45,Work,Wednesday +821,Courtnay Fiorentino,Product Management,2024-08-02,9:19:31,12:19:31,Meeting,Friday +822,Hasty Marciskewski,Product Management,2024-05-03,6:21:35,14:21:35,Other,Friday +823,Beverie Flett,Product Management,2024-04-06,9:14:27,15:14:27,Training,Saturday +824,Barth Eilles,Training,2024-09-02,7:06:41,11:06:41,Work,Monday +825,Bartram Codman,Services,2024-02-06,9:41:14,16:41:14,Work,Tuesday +826,Wolf Micklewright,Support,2024-05-01,8:39:35,16:39:35,Meeting,Wednesday +827,Cassandre Burnhill,Human Resources,2024-09-05,9:06:38,15:06:38,Training,Thursday +828,Eleanor McEvilly,Product Management,2024-11-06,9:53:39,17:53:39,Meeting,Wednesday +829,Lorin Dunbobin,Marketing,2024-01-07,8:50:12,09:50:12,Other,Sunday +830,Arny Lening,Accounting,2024-01-01,9:15:01,16:15:01,Meeting,Monday +831,Jeni Tonkes,Legal,2024-04-02,8:49:14,14:49:14,Other,Tuesday +832,Aubrey Cadany,Accounting,2024-01-07,9:39:00,11:39:00,Meeting,Sunday +833,Lana Derrington,Business Development,2024-08-04,7:38:37,08:38:37,Work,Sunday +834,Marley Jacqueminot,Engineering,2024-10-05,8:42:23,14:42:23,Training,Saturday +835,Marlo Kubera,Marketing,2024-01-03,8:11:35,14:11:35,Client Visit,Wednesday +836,Carmencita Natwick,Engineering,2024-04-02,9:35:18,11:35:18,Meeting,Tuesday +837,Julissa Titchard,Business Development,2024-07-02,8:45:58,12:45:58,Training,Tuesday +838,Harbert Janovsky,Training,2024-01-05,9:35:42,13:35:42,Work,Friday +839,Giusto Toulamain,Services,2024-06-04,6:05:37,14:05:37,Other,Tuesday +840,Svend Limon,Business Development,2024-11-05,6:48:31,08:48:31,Work,Tuesday +841,Maxi Delue,Product Management,2024-01-04,8:06:02,13:06:02,Training,Thursday +842,Hanni Toone,Legal,2024-06-02,8:53:10,11:53:10,Client Visit,Sunday +843,Kendell Tongs,Training,2024-08-03,9:53:16,12:53:16,Other,Saturday +844,Georgena Blanchette,Human Resources,2024-10-03,6:29:10,12:29:10,Work,Thursday +845,Rob Labrone,Product Management,2024-02-01,7:12:12,12:12:12,Work,Thursday +846,Kerry Luciano,Legal,2024-04-07,6:03:49,07:03:49,Training,Sunday +847,Jerrie Nerne,Product Management,2024-03-06,6:35:20,13:35:20,Meeting,Wednesday +848,Cori Dumbelton,Accounting,2024-12-03,6:32:31,09:32:31,Client Visit,Tuesday +849,Carole Archard,Sales,2024-10-04,6:04:47,12:04:47,Client Visit,Friday +850,Desmond Avrahamian,Engineering,2024-11-04,8:07:08,10:07:08,Meeting,Monday +851,Tessy Khosa,Sales,2024-05-07,9:20:05,11:20:05,Other,Tuesday +852,Austin Virgoe,Sales,2024-08-05,6:01:41,09:01:41,Work,Monday +853,Shay MacKibbon,Support,2024-12-07,6:30:56,14:30:56,Work,Saturday +854,Huntlee Ondracek,Product Management,2024-10-06,7:08:30,08:08:30,Work,Sunday +855,Salomi Vischi,Marketing,2024-05-02,6:09:39,14:09:39,Meeting,Thursday +856,Kesley Jurgensen,Product Management,2024-04-03,9:32:46,10:32:46,Work,Wednesday +857,Jordanna Olivazzi,Accounting,2024-11-07,6:48:29,10:48:29,Client Visit,Thursday +858,Devinne Castello,Product Management,2024-06-01,7:54:54,13:54:54,Client Visit,Saturday +859,Natalie Crush,Product Management,2024-09-07,8:47:09,15:47:09,Training,Saturday +860,Analiese Greneham,Product Management,2024-06-01,8:01:36,11:01:36,Meeting,Saturday +861,Gennifer Murtimer,Support,2024-01-02,8:44:21,09:44:21,Meeting,Tuesday +862,Cass Nielson,Legal,2024-09-04,7:19:09,12:19:09,Meeting,Wednesday +863,Verina Dunridge,Support,2024-08-02,8:34:55,09:34:55,Meeting,Friday +864,Mitch Juggins,Services,2024-12-01,6:55:00,14:55:00,Training,Sunday +865,Kanya Clearley,Training,2024-12-06,8:02:07,09:02:07,Other,Friday +866,Rose Kemell,Human Resources,2024-01-06,8:40:20,12:40:20,Training,Saturday +867,Ina Heugle,Business Development,2024-09-01,9:39:51,11:39:51,Meeting,Sunday +868,Rayshell Cotterel,Accounting,2024-03-07,7:59:26,08:59:26,Work,Thursday +869,Chicky Duell,Product Management,2024-07-04,9:15:26,11:15:26,Work,Thursday +870,Eadie Grindall,Sales,2024-06-06,6:09:39,07:09:39,Training,Thursday +871,Lisabeth Lahive,Accounting,2024-01-06,9:28:19,16:28:19,Meeting,Saturday +872,Alene Gliddon,Research and Development,2024-09-03,8:09:07,12:09:07,Meeting,Tuesday +873,Kev Stood,Human Resources,2024-06-07,9:21:56,13:21:56,Other,Friday +874,Julius Phizakarley,Marketing,2024-01-06,9:07:41,14:07:41,Meeting,Saturday +875,Danell Carme,Product Management,2024-12-02,9:37:40,17:37:40,Work,Monday +876,Benny Balcombe,Product Management,2024-09-06,7:26:51,11:26:51,Other,Friday +877,Dewain Hoodlass,Legal,2024-02-05,7:30:45,09:30:45,Work,Monday +878,Graig Housegoe,Product Management,2024-04-04,8:05:54,10:05:54,Meeting,Thursday +879,Dalt Wheble,Support,2024-01-05,6:49:01,12:49:01,Work,Friday +880,Julee Netti,Support,2024-07-01,9:22:13,10:22:13,Work,Monday +881,Gayelord Androck,Engineering,2024-03-05,9:01:43,10:01:43,Work,Tuesday +882,Huberto Templeman,Product Management,2024-05-02,7:04:40,08:04:40,Meeting,Thursday +883,Darlene McLese,Support,2024-07-07,8:15:59,14:15:59,Other,Sunday +884,Dell Rittmeyer,Research and Development,2024-03-05,9:42:06,14:42:06,Meeting,Tuesday +885,Adriano Crossland,Human Resources,2024-04-02,8:08:13,09:08:13,Work,Tuesday +886,Rozanne Ashbridge,Services,2024-09-07,7:32:49,11:32:49,Meeting,Saturday +887,Pearce Frail,Services,2024-06-01,6:38:33,09:38:33,Work,Saturday +888,Marci Lapping,Marketing,2024-08-07,9:19:07,16:19:07,Work,Wednesday +889,Berthe Martinetto,Support,2024-11-04,8:46:11,09:46:11,Work,Monday +890,Andromache Muscott,Services,2024-02-02,6:53:03,11:53:03,Training,Friday +891,Jannel Rushmer,Business Development,2024-06-05,6:45:04,14:45:04,Work,Wednesday +892,Boniface Plume,Support,2024-06-01,7:42:13,14:42:13,Training,Saturday +893,Kris Bohea,Research and Development,2024-05-07,8:48:47,16:48:47,Work,Tuesday +894,Bent Childrens,Services,2024-05-03,7:32:47,09:32:47,Other,Friday +895,Merrielle Farman,Legal,2024-05-02,6:27:41,09:27:41,Training,Thursday +896,Jewelle Luca,Business Development,2024-07-07,9:43:58,11:43:58,Meeting,Sunday +897,Jarid Da Costa,Human Resources,2024-10-05,7:29:24,13:29:24,Training,Saturday +898,Noel Snoxell,Training,2024-02-04,7:17:48,12:17:48,Work,Sunday +899,Germaine Hegdonne,Business Development,2024-07-07,8:45:43,09:45:43,Meeting,Sunday +900,Aigneis Cossum,Business Development,2024-08-04,6:26:17,11:26:17,Work,Sunday +901,Janean Tarbath,Accounting,2024-07-05,6:33:11,12:33:11,Training,Friday +902,Harman Khomin,Accounting,2024-06-06,6:00:58,10:00:58,Meeting,Thursday +903,Abner Huc,Marketing,2024-04-01,6:07:06,13:07:06,Work,Monday +904,Eric Granham,Support,2024-09-02,7:31:50,08:31:50,Work,Monday +905,Kathrine Hancke,Legal,2024-06-06,9:31:27,10:31:27,Meeting,Thursday +906,Callida Stickings,Sales,2024-04-03,7:21:27,12:21:27,Client Visit,Wednesday +907,Cordy Vasyunichev,Business Development,2024-10-03,6:04:51,12:04:51,Meeting,Thursday +908,Fulton Corkish,Services,2024-03-03,9:00:35,14:00:35,Work,Sunday +909,Shelli Medd,Human Resources,2024-06-02,7:23:10,12:23:10,Work,Sunday +910,Joby Bladon,Business Development,2024-12-03,6:04:59,12:04:59,Work,Tuesday +911,Noellyn Komorowski,Engineering,2024-05-02,6:01:44,07:01:44,Other,Thursday +912,Elvera Rey,Engineering,2024-03-07,9:30:08,14:30:08,Meeting,Thursday +913,Arri McArdle,Human Resources,2024-01-05,7:24:32,10:24:32,Work,Friday +914,Dom Tippin,Support,2024-11-03,6:11:30,14:11:30,Work,Sunday +915,Haroun Lofting,Legal,2024-09-03,6:41:51,11:41:51,Training,Tuesday +916,Audy Savage,Product Management,2024-04-03,6:33:11,07:33:11,Other,Wednesday +917,Randall Formilli,Research and Development,2024-07-07,7:45:53,10:45:53,Meeting,Sunday +918,Elysha Ogden,Research and Development,2024-02-02,7:34:25,15:34:25,Work,Friday +919,Garald Lindell,Business Development,2024-01-06,9:03:22,14:03:22,Work,Saturday +920,Lonna Aulds,Marketing,2024-01-02,8:05:45,14:05:45,Work,Tuesday +921,Kevyn Ornillos,Research and Development,2024-09-07,6:01:22,14:01:22,Work,Saturday +922,Lionel Borland,Accounting,2024-10-01,7:39:24,09:39:24,Meeting,Tuesday +923,Marys Lamball,Research and Development,2024-10-07,7:42:18,10:42:18,Work,Monday +924,Chiquia Sobieski,Training,2024-06-01,9:51:47,16:51:47,Client Visit,Saturday +925,Isidro Thumim,Business Development,2024-09-01,7:27:53,09:27:53,Work,Sunday +926,Cosetta Mulrenan,Business Development,2024-06-03,8:44:37,14:44:37,Training,Monday +927,Faythe Sevitt,Engineering,2024-04-01,6:24:22,08:24:22,Other,Monday +928,Ezekiel Sproston,Engineering,2024-07-03,9:02:04,11:02:04,Other,Wednesday +929,Gustavo Kingsbury,Support,2024-07-01,9:00:17,15:00:17,Client Visit,Monday +930,Marlowe Garriock,Business Development,2024-07-02,8:16:10,13:16:10,Meeting,Tuesday +931,Rhea Isaacs,Training,2024-10-03,7:57:40,14:57:40,Work,Thursday +932,Sarene Lewty,Sales,2024-03-02,6:39:02,08:39:02,Work,Saturday +933,Pandora Goldhill,Training,2024-09-05,8:33:28,16:33:28,Other,Thursday +934,Scot Yakovlev,Sales,2024-04-06,6:49:14,11:49:14,Training,Saturday +935,Freddie Errington,Marketing,2024-05-07,9:19:00,16:19:00,Meeting,Tuesday +936,Bone Burgise,Accounting,2024-02-07,7:00:55,09:00:55,Meeting,Wednesday +937,Gallard Brownbill,Product Management,2024-01-04,6:38:35,11:38:35,Work,Thursday +938,Hazel Sherrum,Training,2024-11-07,8:59:40,09:59:40,Client Visit,Thursday +939,Kiersten McCoveney,Training,2024-09-02,7:29:50,08:29:50,Meeting,Monday +940,Conney Wickens,Training,2024-05-01,6:19:23,08:19:23,Work,Wednesday +941,Mackenzie Florence,Support,2024-07-03,6:14:05,14:14:05,Training,Wednesday +942,Blithe Peyto,Legal,2024-05-07,9:15:25,14:15:25,Other,Tuesday +943,Eloise Thombleson,Marketing,2024-10-02,8:11:53,10:11:53,Meeting,Wednesday +944,Ardeen Showering,Training,2024-04-02,6:16:55,13:16:55,Meeting,Tuesday +945,Nadiya Mountjoy,Training,2024-04-02,8:27:59,14:27:59,Work,Tuesday +946,Elka Yitzhak,Legal,2024-10-02,6:45:27,10:45:27,Meeting,Wednesday +947,Deerdre Humphery,Accounting,2024-06-01,9:18:46,14:18:46,Meeting,Saturday +948,Shaun Feakins,Product Management,2024-10-03,7:02:40,08:02:40,Training,Thursday +949,Juliette Ellam,Product Management,2024-04-06,7:01:53,14:01:53,Meeting,Saturday +950,Inge Vanini,Human Resources,2024-07-06,7:54:17,09:54:17,Work,Saturday +951,Gallagher Merryman,Training,2024-11-02,7:09:41,11:09:41,Meeting,Saturday +952,Wilma Largan,Research and Development,2024-06-05,8:24:07,13:24:07,Training,Wednesday +953,Lenore Kobelt,Marketing,2024-02-06,9:23:08,15:23:08,Meeting,Tuesday +954,Korry Wintour,Legal,2024-03-03,7:45:01,09:45:01,Meeting,Sunday +955,Langston Harpur,Engineering,2024-03-07,6:17:36,09:17:36,Work,Thursday +956,Minnie Dabell,Support,2024-02-01,8:05:34,14:05:34,Client Visit,Thursday +957,Kerstin Fadell,Accounting,2024-09-02,8:48:37,13:48:37,Work,Monday +958,Oneida Simenot,Services,2024-12-04,8:38:42,16:38:42,Meeting,Wednesday +959,Sean Roizn,Training,2024-04-01,6:44:09,14:44:09,Client Visit,Monday +960,Guillema L'Episcopio,Services,2024-07-02,8:25:39,15:25:39,Work,Tuesday +961,Gianina Farrens,Research and Development,2024-02-04,9:47:52,12:47:52,Work,Sunday +962,Dewie Handforth,Services,2024-08-05,9:20:55,17:20:55,Training,Monday +963,Guinevere Toupe,Training,2024-08-04,9:17:43,13:17:43,Client Visit,Sunday +964,Clark Dragon,Support,2024-12-06,8:04:38,15:04:38,Work,Friday +965,Raddie Tonbridge,Sales,2024-05-07,8:38:21,11:38:21,Work,Tuesday +966,Richardo Taverner,Support,2024-10-06,9:19:28,13:19:28,Work,Sunday +967,Irita Muller,Accounting,2024-05-07,7:11:42,15:11:42,Meeting,Tuesday +968,Lacie Dran,Accounting,2024-02-04,6:59:03,14:59:03,Work,Sunday +969,Brod Vondrach,Human Resources,2024-03-07,8:29:23,13:29:23,Work,Thursday +970,Lissi Delacourt,Sales,2024-12-02,6:55:13,11:55:13,Work,Monday +971,Lawton McConway,Business Development,2024-12-06,7:24:48,13:24:48,Work,Friday +972,Amalle Oldershaw,Accounting,2024-11-05,8:21:11,15:21:11,Work,Tuesday +973,Mariya Sly,Support,2024-04-03,9:00:12,13:00:12,Other,Wednesday +974,Gris Armal,Services,2024-01-02,7:07:22,08:07:22,Other,Tuesday +975,Debra Esparza,Business Development,2024-06-03,7:37:34,08:37:34,Client Visit,Monday +976,Brew Donnel,Marketing,2024-05-02,6:55:24,10:55:24,Work,Thursday +977,Cathleen Malone,Engineering,2024-07-01,9:10:23,10:10:23,Other,Monday +978,Iosep Delmage,Services,2024-05-07,7:18:09,14:18:09,Training,Tuesday +979,Padraig Rieme,Sales,2024-07-04,9:25:14,12:25:14,Client Visit,Thursday +980,Lurleen Sargeant,Engineering,2024-12-03,6:26:03,13:26:03,Meeting,Tuesday +981,Pru Olwen,Services,2024-09-06,7:18:07,13:18:07,Meeting,Friday +982,Alwin Brilleman,Sales,2024-01-06,6:04:38,08:04:38,Meeting,Saturday +983,Robb Salle,Business Development,2024-03-05,6:40:44,11:40:44,Meeting,Tuesday +984,Lela Varran,Training,2024-08-03,7:22:45,14:22:45,Meeting,Saturday +985,Morry Dorricott,Training,2024-10-07,7:41:33,08:41:33,Meeting,Monday +986,Rodolph Bony,Product Management,2024-04-07,7:57:08,15:57:08,Meeting,Sunday +987,Mortimer Wims,Human Resources,2024-03-01,9:47:13,13:47:13,Meeting,Friday +988,Ignacio Teek,Support,2024-05-07,6:09:17,09:09:17,Training,Tuesday +989,Reagan Makiver,Legal,2024-02-03,7:40:17,10:40:17,Training,Saturday +990,Dawna Tomaszczyk,Product Management,2024-08-03,7:51:47,08:51:47,Meeting,Saturday +991,Devon Gelsthorpe,Engineering,2024-03-05,8:37:15,09:37:15,Work,Tuesday +992,Dulsea Gaskin,Sales,2024-03-04,9:10:53,12:10:53,Work,Monday +993,Franni Moggach,Research and Development,2024-08-06,7:59:44,11:59:44,Meeting,Tuesday +994,Robbin Kineton,Legal,2024-08-07,9:40:22,12:40:22,Work,Wednesday +995,Anatole Ginnelly,Business Development,2024-09-01,9:18:46,15:18:46,Client Visit,Sunday +996,Bax Whisker,Marketing,2024-06-05,6:52:41,07:52:41,Training,Wednesday +997,Carolann Gammie,Marketing,2024-05-03,9:20:39,15:20:39,Training,Friday +998,Cassy Aireton,Business Development,2024-07-04,9:54:07,12:54:07,Meeting,Thursday +999,Hall Sholl,Product Management,2024-06-01,6:22:18,13:22:18,Meeting,Saturday +1000,Cazzie Klishin,Training,2024-09-03,7:12:43,13:12:43,Meeting,Tuesday diff --git a/documentation/occupi-docs/pages/api-documentation/api-usage.mdx b/documentation/occupi-docs/pages/api-documentation/api-usage.mdx index c44bca99..2ea6f3a5 100644 --- a/documentation/occupi-docs/pages/api-documentation/api-usage.mdx +++ b/documentation/occupi-docs/pages/api-documentation/api-usage.mdx @@ -31,7 +31,6 @@ The API also allows you to retrieve information about these resources. - [Image ID](#ImageID) - [Upload Image](#UploadImage) - [Upload Room Image](#UploadRoomImage) - - [Add Room](#AddRoom) ## Api @@ -904,46 +903,4 @@ This endpoint is used to upload an image for a room in the Occupi system. Only A - **Code:** 500 -- **Content:** `{ "status": 500, "message": "Internal server error", "error": {"code":"INTERNAL_SERVER_ERROR","details":null,"message":"Internal server error"} }` - -### Add Room - -This endpoint is used to add a room in the Occupi system. Only Admins can add rooms. - -- **URL** - - `/api/add-room` - -- **Method** - - `PUT` - -- **Request Body** - -- **Content** - -```json copy -{ - "roomId": "RM000", // required ensure that the room id is unique and starts with RM and no room with the same id exists - "roomNo": "1", // required ensure that the room number is unique and no room with the same number exists - "floorNo": 3, // required - "minOccupancy": 1, // required - "maxOccupancy": 10, // required - "description": "This is a room", // required - "resources": ["projector", "whiteboard"], // required - "roomName": "Room 1", // required -} -``` - -**Success Response** - -- **Code:** 200 - -- **Content:** `{ "status": 200, "message": "Successfully added room!", "data": null }` - -**Error Response** - -- **Code:** 500 - -- **Content:** `{ "status": 500, "message": "Internal server error", "error": {"code":"INTERNAL_SERVER_ERROR","details":null,"message":"Internal server error"} }` - +- **Content:** `{ "status": 500, "message": "Internal server error", "error": {"code":"INTERNAL_SERVER_ERROR","details":null,"message":"Internal server error"} }` \ No newline at end of file diff --git a/documentation/occupi-docs/pages/api-documentation/auth-usage.mdx b/documentation/occupi-docs/pages/api-documentation/auth-usage.mdx index f364fc9f..3e1c4ba2 100644 --- a/documentation/occupi-docs/pages/api-documentation/auth-usage.mdx +++ b/documentation/occupi-docs/pages/api-documentation/auth-usage.mdx @@ -323,8 +323,7 @@ This endpoint is used for finishing the authentication process using webauthn fo { "email": "abcd@gmail.com", "password": "123456", - "employee_id": "OCCUPI20241234", //this field is optional, an employee id will be generated if not provided - "expoPushToken": "Push token goes here" + "employee_id": "OCCUPI20241234" //this field is optional, an employee id will be generated if not provided } ``` diff --git a/frontend/occupi-desktop/src/App.css b/frontend/occupi-desktop/src/App.css new file mode 100644 index 00000000..a89ebd15 --- /dev/null +++ b/frontend/occupi-desktop/src/App.css @@ -0,0 +1,7 @@ +.logo.vite:hover { + filter: drop-shadow(0 0 2em #747bff); +} + +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafb); +} diff --git a/frontend/occupi-desktop/src/assets/react.svg b/frontend/occupi-desktop/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/frontend/occupi-desktop/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/occupi-desktop/src/components/drawerComponent/DrawerComponent.test.tsx b/frontend/occupi-desktop/src/components/drawerComponent/DrawerComponent.test.tsx index 4fbd89db..c11ee2e8 100644 --- a/frontend/occupi-desktop/src/components/drawerComponent/DrawerComponent.test.tsx +++ b/frontend/occupi-desktop/src/components/drawerComponent/DrawerComponent.test.tsx @@ -1,4 +1,4 @@ - +import { expect, test, afterEach } from "bun:test"; import { render, screen, fireEvent, cleanup } from "@testing-library/react"; import DrawerComponent from "./DrawerComponent"; import { BrowserRouter } from "react-router-dom"; diff --git a/frontend/occupi-desktop/src/components/index.ts b/frontend/occupi-desktop/src/components/index.ts index 6696deab..5ac24eb0 100644 --- a/frontend/occupi-desktop/src/components/index.ts +++ b/frontend/occupi-desktop/src/components/index.ts @@ -9,7 +9,7 @@ import GraphContainer from "./graphContainer/GraphContainer"; import TabComponent from "./tabComponent/TabComponent"; import Appearance from "./appearance/Appearance"; import MenuItem from './drawerComponent/menuItem/MenuItem'; -// import Bookings from "./bookings/Bookings"; +import Bookings from "./bookings/Bookings"; import OverView from "./overView/OverView"; import OverviewComponent from "./OverviewComponent/OverviewComponent"; import ProfileComponent from "./ProfileComponent/ProfileComponent"; @@ -33,7 +33,7 @@ export { TabComponent, Appearance, MenuItem, - // Bookings, + Bookings, OverView, OverviewComponent, ProfileComponent, diff --git a/frontend/occupi-desktop/src/styles.css b/frontend/occupi-desktop/src/styles.css new file mode 100644 index 00000000..f7de85bf --- /dev/null +++ b/frontend/occupi-desktop/src/styles.css @@ -0,0 +1,109 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color: #0f0f0f; + background-color: #f6f6f6; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +.container { + margin: 0; + padding-top: 10vh; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: 0.75s; +} + +.logo.tauri:hover { + filter: drop-shadow(0 0 2em #24c8db); +} + +.row { + display: flex; + justify-content: center; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +h1 { + text-align: center; +} + +input, +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + color: #0f0f0f; + background-color: #ffffff; + transition: border-color 0.25s; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); +} + +button { + cursor: pointer; +} + +button:hover { + border-color: #396cd8; +} +button:active { + border-color: #396cd8; + background-color: #e8e8e8; +} + +input, +button { + outline: none; +} + +#greet-input { + margin-right: 5px; +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f6f6f6; + background-color: #2f2f2f; + } + + a:hover { + color: #24c8db; + } + + input, + button { + color: #ffffff; + background-color: #0f0f0f98; + } + button:active { + background-color: #0f0f0f69; + } +} diff --git a/frontend/occupi-mobile4/app.json b/frontend/occupi-mobile4/app.json index 133db28a..967974a1 100644 --- a/frontend/occupi-mobile4/app.json +++ b/frontend/occupi-mobile4/app.json @@ -18,7 +18,7 @@ "usesNonExemptEncryption": false }, "infoPlist": { - "NSFaceIDUsageDescription": "This app uses Face ID or Touch ID to confirm bookings." + "NSFaceIDUsageDescription": "Face ID helps to verify your identity, providing a quick and secure way to log into your account." } }, "android": { @@ -45,6 +45,12 @@ { "faceIDPermission": "Allow $(PRODUCT_NAME) to access your Face ID biometric data." } + ], + [ + "expo-local-authentication", + { + "faceIDPermission": "Allow $(PRODUCT_NAME) to use Face ID." + } ] ], "experiments": { diff --git a/frontend/occupi-mobile4/app/_layout.tsx b/frontend/occupi-mobile4/app/_layout.tsx index 5decefd6..0ebbed75 100644 --- a/frontend/occupi-mobile4/app/_layout.tsx +++ b/frontend/occupi-mobile4/app/_layout.tsx @@ -6,6 +6,7 @@ import { Stack } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; import 'react-native-reanimated'; import { GluestackUIProvider } from "@gluestack-ui/themed"; +import { ThemeProvider } from '@/components/ThemeContext'; import { NavBarProvider } from '@/components/NavBarProvider'; import { config } from "@gluestack-ui/config"; // Optional if you want to use default theme @@ -32,36 +33,38 @@ export default function RootLayout() { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } \ No newline at end of file diff --git a/frontend/occupi-mobile4/components/NavBar.tsx b/frontend/occupi-mobile4/components/NavBar.tsx index ddeef052..f67ac580 100644 --- a/frontend/occupi-mobile4/components/NavBar.tsx +++ b/frontend/occupi-mobile4/components/NavBar.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { StyleSheet } from 'react-native'; import { Text, Button, Icon, CalendarDaysIcon, BellIcon } from '@gluestack-ui/themed'; import { Feather } from '@expo/vector-icons'; @@ -6,25 +6,38 @@ import { FontAwesome6, Ionicons } from '@expo/vector-icons'; import { router } from 'expo-router'; import { BlurView } from 'expo-blur'; import { useColorScheme } from 'react-native'; +import * as SecureStore from 'expo-secure-store'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import { useNavBar } from './NavBarProvider'; +import { useTheme } from './ThemeContext'; const NavBar = () => { - let colorScheme = useColorScheme(); - const styles = getStyles(colorScheme); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const styles = getStyles(currentTheme); + const [accentColour, setAccentColour] = useState('greenyellow'); const { currentTab, setCurrentTab } = useNavBar(); const handleTabPress = (tabName, route) => { setCurrentTab(tabName); router.replace(route); }; + + useEffect(() => { + const getSettings = async () => { + let accentcolour = await SecureStore.getItemAsync('accentColour'); + setAccentColour(accentcolour); + }; + getSettings(); +}, []); // console.log(currentTab); return ( @@ -40,13 +53,13 @@ const NavBar = () => { Home @@ -63,13 +76,13 @@ const NavBar = () => { My bookings @@ -87,13 +100,13 @@ const NavBar = () => { as={CalendarDaysIcon} w={hp('3%')} h={hp('3%')} - color={currentTab === 'Book' ? 'yellowgreen' : colorScheme === 'dark' ? 'white' : 'black'} + color={currentTab === 'Book' ? `${accentColour}` : currentTheme === 'dark' ? 'white' : 'black'} /> Book @@ -111,14 +124,14 @@ const NavBar = () => { as={BellIcon} w={hp('3%')} h={hp('3%')} - color={currentTab === 'Notifications' ? 'yellowgreen' : colorScheme === 'dark' ? 'white' : 'black'} + color={currentTab === 'Notifications' ? `${accentColour}` : currentTheme === 'dark' ? 'white' : 'black'} /> Notifications @@ -135,14 +148,14 @@ const NavBar = () => { Profile @@ -151,7 +164,7 @@ const NavBar = () => { ); }; -const getStyles = (colorScheme) => StyleSheet.create({ +const getStyles = (currentTheme) => StyleSheet.create({ container: { position: 'absolute', bottom: 0, @@ -161,10 +174,10 @@ const getStyles = (colorScheme) => StyleSheet.create({ paddingBottom: hp('3%'), flexDirection: 'row', justifyContent: 'space-around', - backgroundColor: colorScheme === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.5)', + backgroundColor: currentTheme === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.5)', paddingVertical: hp('1%'), borderTopWidth: 1, - borderTopColor: colorScheme === 'dark' ? '#444' : '#ccc', + borderTopColor: currentTheme === 'dark' ? '#444' : '#ccc', borderLeftColor: '#ccc', borderRightColor: '#ccc', } diff --git a/frontend/occupi-mobile4/components/ThemeContext.tsx b/frontend/occupi-mobile4/components/ThemeContext.tsx new file mode 100644 index 00000000..acef058e --- /dev/null +++ b/frontend/occupi-mobile4/components/ThemeContext.tsx @@ -0,0 +1,72 @@ +// ThemeContext.tsx + +import React, { createContext, useState, useEffect, ReactNode } from 'react'; +import * as SecureStore from 'expo-secure-store'; +import { View, ActivityIndicator, useColorScheme } from 'react-native'; + +// Define the shape of the context state +interface ThemeContextType { + theme: string; + setTheme: (theme: string) => void; +} + +// Create the context +const ThemeContext = createContext(undefined); + +// Create a provider component +export const ThemeProvider = ({ children }: { children: ReactNode }) => { + const [theme, setTheme] = useState(null); // Start with null to indicate loading + + useEffect(() => { + const fetchTheme = async () => { + try { + const storedTheme = await SecureStore.getItemAsync('Theme'); + if (storedTheme) { + setTheme(storedTheme); + } else { + // If no theme is found, set a default theme + setTheme('light'); + } + } catch (error) { + console.error('Failed to load theme from SecureStore:', error); + setTheme('light'); // Set a default theme in case of an error + } + }; + + fetchTheme(); + }, []); + + const updateTheme = async (newTheme: string) => { + console.log('updating theme'); + try { + await SecureStore.setItemAsync('Theme', newTheme); + setTheme(newTheme); + } catch (error) { + console.error('Failed to save theme to SecureStore:', error); + } + }; + + if (theme === null) { + // Show a loading spinner while the theme is being fetched + return ( + + + + ); + } + + return ( + + {children} + + ); +}; + +// Custom hook to use the ThemeContext +export const useTheme = (): ThemeContextType => { + const context = React.useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; diff --git a/frontend/occupi-mobile4/components/__tests__/NavBar-test.tsx b/frontend/occupi-mobile4/components/__tests__/NavBar-test.tsx index 223c2694..e5af1344 100644 --- a/frontend/occupi-mobile4/components/__tests__/NavBar-test.tsx +++ b/frontend/occupi-mobile4/components/__tests__/NavBar-test.tsx @@ -1,9 +1,23 @@ import * as React from 'react'; import renderer from 'react-test-renderer'; import NavBar from '../NavBar'; +import { useNavBar } from '../NavBarProvider'; -it(`renders correctly`, () => { - const tree = renderer.create(Snapshot test!).toJSON(); +// Mock the NavBarProvider module +jest.mock('../NavBarProvider', () => ({ + useNavBar: jest.fn(), +})); - expect(tree).toMatchSnapshot(); -}); +describe('NavBar', () => { + it(`renders correctly`, () => { + // Mock the useNavBar hook implementation + (useNavBar as jest.Mock).mockReturnValue({ + currentTab: 'Home', + setCurrentTab: jest.fn(), + }); + + const tree = renderer.create().toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/frontend/occupi-mobile4/components/__tests__/__snapshots__/NavBar-test.tsx.snap b/frontend/occupi-mobile4/components/__tests__/__snapshots__/NavBar-test.tsx.snap index 4687366d..5646babc 100644 --- a/frontend/occupi-mobile4/components/__tests__/__snapshots__/NavBar-test.tsx.snap +++ b/frontend/occupi-mobile4/components/__tests__/__snapshots__/NavBar-test.tsx.snap @@ -1,5 +1,922 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`NavBar renders correctly 1`] = ` + + + + + + Home + + + + + + My bookings + + + + + + + + + + + + + + + + + + + + + Book + + + + + + + + + + + + + Notifications + + + + + + Profile + + + +`; + exports[`renders correctly 1`] = ` { + await SecureStore.setItemAsync('UserData', value); + }, + + storeToken: async (value) => { + await SecureStore.setItemAsync('Token', value); + }, + + storeUserEmail: async (value) => { + await SecureStore.setItemAsync('Email', value); + }, + + getToken: async () => { + return await SecureStore.getItemAsync('Token'); + } +}; + +export default LoginModel; diff --git a/frontend/occupi-mobile4/models/data.ts b/frontend/occupi-mobile4/models/data.ts new file mode 100644 index 00000000..80fc0594 --- /dev/null +++ b/frontend/occupi-mobile4/models/data.ts @@ -0,0 +1,52 @@ +//these models describe the room structure as well as booking information structure + +export interface Room { + description: string; + floorNo: string; + maxOccupancy: string; + minOccupancy:string; + roomId: string; + roomName: string; + roomNo: string; +} + +export interface Booking { + checkedIn: boolean; + creator: string; + date: string; + emails: string[]; + end: string; + floorNo: string; + occupiId: string; + roomId: string; + roomName: string; + start: string; +} + +export interface User { + email: string; + name: string; + dob: string; + gender: "Male" | "Female" | "Other"; + session_email: string; + employeeid: string; + number: string; + pronouns?: string; +} + +export interface Notification { + message: string; + send_time: string; + title: string; + unreadEmails: string[]; +} + +export interface SecuritySettings { + mfa: "on" | "off"; + forceLogout: "on" | "off"; +} + +export interface NotificationSettings { + invites: "on" | "off"; + bookingReminder: "on" | "off"; +} \ No newline at end of file diff --git a/frontend/occupi-mobile4/models/requests.ts b/frontend/occupi-mobile4/models/requests.ts new file mode 100644 index 00000000..b9907cae --- /dev/null +++ b/frontend/occupi-mobile4/models/requests.ts @@ -0,0 +1,127 @@ +//these models describe data to be sent to the api (POST body) + +/* ---Auth Requests--- */ + +export interface LoginReq { + email: string; + password: string; +} + +export interface RegisterReq { + email: string; + password: string; + employee_id?: string; + expoPushToken: string; +} + +export interface VerifyOTPReq { + email: string; + otp: string; +} + +export interface ResetPasswordReq { + email: string; + newPassword: string; + newPasswordConfirm: string; + otp: string; +} + +/* ---API Requests--- */ + +//Rooms & Bookings + +export interface BookRoomReq { + roomId: string; + roomName: string; + emails: string[]; + creator: string; + floorNo: string; //string integer + date: string; + start: string; + end: string; +} + +export interface ViewBookingsReq { + operator: string; + filter: { + email: string; + date?: string; + }; + order_asc?: string; + order_desc?: string; + projection?: string[]; + limit?: number; + page?: number; +} + +export interface ViewRoomsReq { + operator: string; + filter?: { + floorNo: string; + }; + order_asc?: string; + order_desc?: string; + projection?: string[]; + limit?: number; + page?: number; +} + +export interface CancelBookingReq { + bookingId: string; + roomId: string; + emails: string[]; + creator: string; + floorNo: string; //string integer + date: string; + start: string; + end: string; + roomName: string; +} + +export interface CheckInReq { + bookingId: string; + email: string; +} + + +//Users + +export interface UpdateDetailsReq { + email?: string; + name?: string; + dob?: string; + gender?: string; + session_email: string; + employeeid?: string; + number?: string; + pronouns?: string; +} + +export interface NotificationsReq { + operator: string; + filter?: { + emails: string[]; + }; + order_asc?: string; + order_desc?: string; + projection?: string[]; + limit?: number; + page?: number; +} + +//Updating settings + +export interface SecuritySettingsReq { + email: string; + mfa?: "on" | "off"; + forceLogout?: "on" | "off"; + currentPassword?: string; + newPassword?: string; + newPasswordConfirm?: string; +} + +export interface NotificationSettingsReq { + email: string; + invites: "on" | "off"; + bookingReminder: "on" | "off"; +} \ No newline at end of file diff --git a/frontend/occupi-mobile4/models/response.ts b/frontend/occupi-mobile4/models/response.ts new file mode 100644 index 00000000..cf451700 --- /dev/null +++ b/frontend/occupi-mobile4/models/response.ts @@ -0,0 +1,28 @@ +//these models describe the structure of the responses from the api + +export interface Error { + code: string; + details: string; + message: string; +} +export interface Unsuccessful { + data: null; + status: 'error'; + message: string; + error: Error; +} + +export interface LoginSuccess { + data: { + token: string; + }; + message: string; + status: number; +} + +export interface Success { + status: number; + message: string; + data: any; +} + diff --git a/frontend/occupi-mobile4/package-lock.json b/frontend/occupi-mobile4/package-lock.json index 63519c8d..f657ff76 100644 --- a/frontend/occupi-mobile4/package-lock.json +++ b/frontend/occupi-mobile4/package-lock.json @@ -23,6 +23,7 @@ "@ui-kitten/components": "^5.3.1", "axios": "^1.7.2", "date-fns": "^3.6.0", + "dotenv": "^16.4.5", "expo": "~51.0.14", "expo-blur": "^13.0.2", "expo-constants": "~16.0.2", @@ -68,7 +69,9 @@ }, "devDependencies": { "@babel/core": "^7.20.0", + "@types/axios": "^0.14.0", "@types/jest": "^29.5.12", + "@types/node": "^22.0.0", "@types/react": "~18.2.45", "@types/react-test-renderer": "^18.0.7", "eslint": "^8.57.0", @@ -76,9 +79,23 @@ "jest": "^29.7.0", "jest-expo": "~51.0.1", "react-test-renderer": "18.2.0", + "ts-jest": "^29.2.3", + "ts-node": "^10.9.2", "typescript": "~5.3.3" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -165,6 +182,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "peer": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", @@ -470,6 +500,70 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", + "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", + "peer": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", + "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", + "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", + "peer": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-proposal-async-generator-functions": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", @@ -635,6 +729,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "peer": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -650,7 +756,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -662,7 +768,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, @@ -670,6 +775,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-decorators": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.7.tgz", @@ -734,11 +854,40 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -750,7 +899,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -856,7 +1004,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -881,6 +1028,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", @@ -895,6 +1058,24 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz", + "integrity": "sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==", + "peer": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-async-to-generator": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", @@ -911,6 +1092,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-block-scoping": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", @@ -925,6 +1121,39 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, "node_modules/@babel/plugin-transform-classes": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", @@ -975,6 +1204,69 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "peer": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", @@ -1005,6 +1297,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-function-name": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", @@ -1021,6 +1329,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-literals": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", @@ -1035,6 +1359,53 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", @@ -1051,6 +1422,40 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", + "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", + "peer": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", @@ -1066,6 +1471,21 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", @@ -1081,6 +1501,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", @@ -1098,6 +1534,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-optional-chaining": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", @@ -1160,6 +1628,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-display-name": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", @@ -1249,17 +1732,105 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-runtime": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz", "integrity": "sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.1", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "semver": "^6.3.1" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.1", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1268,10 +1839,11 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-shorthand-properties": { + "node_modules/@babel/plugin-transform-typeof-symbol": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", - "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", + "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" }, @@ -1282,13 +1854,15 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-spread": { + "node_modules/@babel/plugin-transform-typescript": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", - "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz", + "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==", "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@babel/plugin-syntax-typescript": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1297,10 +1871,11 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-sticky-regex": { + "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", - "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" }, @@ -1311,11 +1886,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-template-literals": { + "node_modules/@babel/plugin-transform-unicode-property-regex": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", - "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "peer": true, "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { @@ -1325,15 +1902,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-typescript": { + "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz", - "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-typescript": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1342,10 +1917,11 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-unicode-regex": { + "node_modules/@babel/plugin-transform-unicode-sets-regex": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", - "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", + "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "peer": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.24.7", "@babel/helper-plugin-utils": "^7.24.7" @@ -1353,6 +1929,101 @@ "engines": { "node": ">=6.9.0" }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", + "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.24.7", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.24.7", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-modules-systemjs": "^7.24.7", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, "peerDependencies": { "@babel/core": "^7.0.0-0" } @@ -1373,6 +2044,20 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/preset-react": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", @@ -1514,7 +2199,29 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true + "devOptional": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", @@ -3674,7 +4381,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, + "devOptional": true, "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -3690,7 +4397,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, + "devOptional": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -3703,7 +4410,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, + "devOptional": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -3715,7 +4422,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, + "devOptional": true, "dependencies": { "p-try": "^2.0.0" }, @@ -3730,7 +4437,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, + "devOptional": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -3742,7 +4449,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -3751,7 +4458,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -3768,7 +4475,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3783,7 +4490,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3799,7 +4506,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3811,13 +4518,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/@jest/console/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -3826,7 +4533,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3838,7 +4545,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/reporters": "^29.7.0", @@ -3885,7 +4592,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3900,7 +4607,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3916,7 +4623,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3928,13 +4635,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/@jest/core/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -3943,7 +4650,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3980,7 +4687,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, + "devOptional": true, "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" @@ -3993,7 +4700,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, + "devOptional": true, "dependencies": { "jest-get-type": "^29.6.3" }, @@ -4021,7 +4728,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -4036,7 +4743,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, + "devOptional": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", @@ -4079,7 +4786,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -4094,7 +4801,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4110,7 +4817,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -4122,13 +4829,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/@jest/reporters/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -4137,7 +4844,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -4160,7 +4867,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", @@ -4174,7 +4881,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", @@ -4189,7 +4896,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", @@ -4204,7 +4911,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -4230,7 +4937,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -4245,7 +4952,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4261,7 +4968,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -4273,13 +4980,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/@jest/transform/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -4288,7 +4995,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -4300,7 +5007,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, + "devOptional": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -6648,6 +7355,25 @@ "node": ">=8" } }, + "node_modules/@react-native-community/datetimepicker": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.1.0.tgz", + "integrity": "sha512-7WCP4U+cOiTFOxGRb7+w2kzIDDqI0xb2D62dbF1qt8C06KmWIKcNgmzx7gfeCjzJMIDK8v+PoUECU2YZz5PvCQ==", + "peer": true, + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-windows": "*" + }, + "peerDependenciesMeta": { + "react-native-windows": { + "optional": true + } + } + }, "node_modules/@react-native-cookies/cookies": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@react-native-cookies/cookies/-/cookies-6.2.1.tgz", @@ -7838,6 +8564,15 @@ "node": ">=14.15" } }, + "node_modules/@rnx-kit/chromium-edge-launcher/node_modules/@types/node": { + "version": "18.19.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.42.tgz", + "integrity": "sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@rnx-kit/chromium-edge-launcher/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -7864,6 +8599,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@rnx-kit/chromium-edge-launcher/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/@segment/loosely-validate-event": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz", @@ -7950,11 +8691,46 @@ "node": ">= 10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "devOptional": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true + }, + "node_modules/@types/axios": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", + "integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==", + "deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -7967,7 +8743,7 @@ "version": "7.6.8", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/types": "^7.0.0" } @@ -7976,7 +8752,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -7986,7 +8762,7 @@ "version": "7.20.6", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/types": "^7.20.7" } @@ -8000,7 +8776,7 @@ "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, + "devOptional": true, "dependencies": { "@types/node": "*" } @@ -8036,6 +8812,7 @@ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", "dev": true, + "license": "MIT", "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -8064,11 +8841,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.19.34", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", - "integrity": "sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.0.tgz", + "integrity": "sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.11.1" } }, "node_modules/@types/node-forge": { @@ -8083,18 +8861,28 @@ "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.2.79", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.79.tgz", "integrity": "sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, + "node_modules/@types/react-native": { + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.73.0.tgz", + "integrity": "sha512-6ZRPQrYM72qYKGWidEttRe6M5DZBEV5F+MHMHqd4TTYx0tfkcdrUFGdef6CCxY0jXU7wldvd/zA/b0A/kTeJmA==", + "deprecated": "This is a stub types definition. react-native provides its own type definitions, so you do not need this installed.", + "peer": true, + "dependencies": { + "react-native": "*" + } + }, "node_modules/@types/react-test-renderer": { "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.3.0.tgz", @@ -8487,7 +9275,7 @@ "version": "8.3.3", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", - "dev": true, + "devOptional": true, "dependencies": { "acorn": "^8.11.0" }, @@ -8882,6 +9670,13 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true, + "license": "MIT" + }, "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -8949,7 +9744,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -8970,7 +9765,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -8985,7 +9780,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -9001,7 +9796,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -9013,13 +9808,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/babel-jest/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -9028,7 +9823,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -9040,7 +9835,7 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -9056,7 +9851,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -9072,7 +9867,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -9136,7 +9931,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", @@ -9175,7 +9970,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, + "devOptional": true, "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" @@ -9235,6 +10030,18 @@ "node": ">=0.6" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -9333,6 +10140,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -9526,7 +10346,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } @@ -9539,6 +10359,24 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001636", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", @@ -9583,7 +10421,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10" } @@ -9596,6 +10434,30 @@ "node": "*" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "peer": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -9639,7 +10501,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", - "dev": true + "devOptional": true }, "node_modules/clean-stack": { "version": "2.2.0", @@ -9717,7 +10579,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, + "devOptional": true, "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" @@ -9727,7 +10589,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true + "devOptional": true }, "node_modules/color": { "version": "4.2.3", @@ -9962,7 +10824,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -9983,7 +10845,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -9998,7 +10860,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -10014,7 +10876,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -10026,13 +10888,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/create-jest/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -10041,7 +10903,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10049,6 +10911,12 @@ "node": ">=8" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true + }, "node_modules/cross-fetch": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", @@ -10086,6 +10954,15 @@ "node": ">=8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "peer": true, + "engines": { + "node": ">=4" + } + }, "node_modules/css-in-js-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", @@ -10094,6 +10971,12 @@ "hyphenate-style-name": "^1.0.3" } }, + "node_modules/css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==", + "peer": true + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -10109,6 +10992,17 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "peer": true, + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", @@ -10140,6 +11034,18 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "peer": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -10300,7 +11206,7 @@ "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", - "dev": true, + "devOptional": true, "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -10484,9 +11390,24 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "peer": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, "engines": { - "node": ">=8" + "node": ">=0.3.1" } }, "node_modules/diff-sequences": { @@ -10508,6 +11429,12 @@ "node": ">=8" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "peer": true + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -10628,6 +11555,22 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.803", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.803.tgz", @@ -10637,7 +11580,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=12" }, @@ -10658,6 +11601,16 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -11477,7 +12430,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11585,7 +12537,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">= 0.8.0" } @@ -11594,7 +12546,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", @@ -12028,6 +12980,12 @@ "expo": "*" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "peer": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -12052,7 +13010,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "devOptional": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -12066,9 +13024,9 @@ "integrity": "sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg==" }, "node_modules/fast-xml-parser": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz", - "integrity": "sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", "funding": [ { "type": "github", @@ -12079,6 +13037,7 @@ "url": "https://paypal.me/naturalintelligence" } ], + "license": "MIT", "dependencies": { "strnum": "^1.0.5" }, @@ -12151,6 +13110,39 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -12600,7 +13592,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8.0.0" } @@ -12931,7 +13923,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "devOptional": true }, "node_modules/http-errors": { "version": "2.0.0", @@ -12999,7 +13991,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, + "devOptional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -13072,7 +14064,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, + "devOptional": true, "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -13253,6 +14245,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "peer": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -13377,7 +14381,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } @@ -13731,7 +14735,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -13740,7 +14744,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", @@ -13756,7 +14760,7 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, + "devOptional": true, "bin": { "semver": "bin/semver.js" }, @@ -13768,7 +14772,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, + "devOptional": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -13782,7 +14786,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -13791,7 +14795,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -13803,7 +14807,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, + "devOptional": true, "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", @@ -13817,7 +14821,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -13826,7 +14830,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, + "devOptional": true, "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -13865,11 +14869,107 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, + "devOptional": true, + "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -13895,7 +14995,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, + "devOptional": true, "dependencies": { "execa": "^5.0.0", "jest-util": "^29.7.0", @@ -13909,7 +15009,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, + "devOptional": true, "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -13932,7 +15032,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -13944,7 +15044,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" }, @@ -13956,7 +15056,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } @@ -13965,7 +15065,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, + "devOptional": true, "dependencies": { "path-key": "^3.0.0" }, @@ -13977,7 +15077,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, + "devOptional": true, "dependencies": { "mimic-fn": "^2.1.0" }, @@ -13992,7 +15092,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -14023,7 +15123,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -14038,7 +15138,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14054,7 +15154,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -14066,13 +15166,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/jest-circus/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -14081,7 +15181,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -14093,7 +15193,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", @@ -14126,7 +15226,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -14141,7 +15241,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14157,7 +15257,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -14169,13 +15269,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/jest-cli/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -14184,7 +15284,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -14196,7 +15296,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", @@ -14241,7 +15341,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -14256,7 +15356,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14272,7 +15372,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -14284,13 +15384,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/jest-config/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -14299,7 +15399,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -14389,7 +15489,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, + "devOptional": true, "dependencies": { "detect-newline": "^3.0.0" }, @@ -14401,7 +15501,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -14417,7 +15517,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -14432,7 +15532,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14448,7 +15548,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -14460,13 +15560,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/jest-each/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -14475,7 +15575,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -14561,7 +15661,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -14586,7 +15686,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, + "devOptional": true, "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" @@ -14773,7 +15873,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" }, @@ -14790,7 +15890,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, + "devOptional": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -14799,7 +15899,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, + "devOptional": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", @@ -14819,7 +15919,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, + "devOptional": true, "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" @@ -14832,7 +15932,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -14847,7 +15947,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14863,7 +15963,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -14875,13 +15975,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/jest-resolve/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -14890,7 +15990,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -14902,7 +16002,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", @@ -14934,7 +16034,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -14949,7 +16049,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14965,7 +16065,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -14977,13 +16077,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/jest-runner/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -14992,7 +16092,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -15001,7 +16101,7 @@ "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, + "devOptional": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -15011,7 +16111,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -15023,7 +16123,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -15056,7 +16156,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -15071,7 +16171,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -15087,7 +16187,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -15099,13 +16199,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/jest-runtime/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -15114,7 +16214,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -15126,7 +16226,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", @@ -15157,7 +16257,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -15172,7 +16272,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -15188,7 +16288,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -15200,13 +16300,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/jest-snapshot/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -15215,7 +16315,7 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, + "devOptional": true, "bin": { "semver": "bin/semver.js" }, @@ -15227,7 +16327,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -15666,7 +16766,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, + "devOptional": true, "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", @@ -15685,7 +16785,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -15700,7 +16800,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -15716,7 +16816,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -15728,13 +16828,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/jest-watcher/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -15743,7 +16843,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -15792,6 +16892,15 @@ "resolved": "https://registry.npmjs.org/jimp-compact/-/jimp-compact-0.16.1.tgz", "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==" }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/joi": { "version": "17.13.1", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.1.tgz", @@ -16017,7 +17126,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "devOptional": true }, "node_modules/json-schema-deref-sync": { "version": "0.13.0", @@ -16337,6 +17446,15 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "peer": true, + "engines": { + "node": ">=10" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -16376,6 +17494,13 @@ "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", "integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -16582,7 +17707,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, + "devOptional": true, "dependencies": { "semver": "^7.5.3" }, @@ -16597,7 +17722,7 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, + "devOptional": true, "bin": { "semver": "bin/semver.js" }, @@ -16605,6 +17730,12 @@ "node": ">=10" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -17330,7 +18461,6 @@ "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "optional": true, "engines": { "node": "*" } @@ -17427,14 +18557,73 @@ "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nativewind": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/nativewind/-/nativewind-2.0.11.tgz", + "integrity": "sha512-qCEXUwKW21RYJ33KRAJl3zXq2bCq82WoI564fI21D/TiqhfmstZOqPN53RF8qK1NDK6PGl56b2xaTxgObEePEg==", + "peer": true, + "dependencies": { + "@babel/generator": "^7.18.7", + "@babel/helper-module-imports": "7.18.6", + "@babel/types": "7.19.0", + "css-mediaquery": "^0.1.2", + "css-to-react-native": "^3.0.0", + "micromatch": "^4.0.5", + "postcss": "^8.4.12", + "postcss-calc": "^8.2.4", + "postcss-color-functional-notation": "^4.2.2", + "postcss-css-variables": "^0.18.0", + "postcss-nested": "^5.0.6", + "react-is": "^18.1.0", + "use-sync-external-store": "^1.1.0" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "tailwindcss": "~3" + } + }, + "node_modules/nativewind/node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "peer": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/nativewind/node_modules/@babel/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", + "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", + "peer": true, + "dependencies": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, + "node_modules/nativewind/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "peer": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "devOptional": true }, "node_modules/ncp": { "version": "2.0.0", @@ -17660,6 +18849,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "peer": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -17994,7 +19192,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -18152,7 +19350,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, + "devOptional": true, "dependencies": { "find-up": "^4.0.0" }, @@ -18164,7 +19362,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, + "devOptional": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -18177,7 +19375,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, + "devOptional": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -18189,7 +19387,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, + "devOptional": true, "dependencies": { "p-try": "^2.0.0" }, @@ -18204,7 +19402,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, + "devOptional": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -18300,6 +19498,176 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "peer": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", + "peer": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-css-variables": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/postcss-css-variables/-/postcss-css-variables-0.18.0.tgz", + "integrity": "sha512-lYS802gHbzn1GI+lXvy9MYIYDuGnl1WB4FTKoqMQqJ3Mab09A7a/1wZvGTkCEZJTM8mSbIyb1mJYn8f0aPye0Q==", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "escape-string-regexp": "^1.0.3", + "extend": "^3.0.1" + }, + "peerDependencies": { + "postcss": "^8.2.6" + } + }, + "node_modules/postcss-css-variables/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "peer": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "peer": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "peer": true, + "dependencies": { + "postcss-selector-parser": "^6.0.6" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "peer": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -18429,7 +19797,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "individual", @@ -19141,7 +20509,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", "integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==", - "dev": true, "dependencies": { "react-is": "^18.2.0", "react-shallow-renderer": "^16.15.0", @@ -19154,8 +20521,25 @@ "node_modules/react-test-renderer/node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "peer": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } }, "node_modules/readable-stream": { "version": "2.3.8", @@ -19171,6 +20555,30 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "peer": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/readline": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", @@ -19266,6 +20674,15 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -19391,7 +20808,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, + "devOptional": true, "dependencies": { "resolve-from": "^5.0.0" }, @@ -19532,7 +20949,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "devOptional": true }, "node_modules/sax": { "version": "1.4.1", @@ -20072,7 +21489,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, + "devOptional": true, "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" @@ -20223,7 +21640,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -20259,7 +21676,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" }, @@ -20404,6 +21821,74 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/tailwindcss": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", + "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "peer": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "peer": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tailwindcss/node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "peer": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -20587,7 +22072,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, + "devOptional": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -20757,6 +22242,117 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/ts-jest": { + "version": "29.2.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.3.tgz", + "integrity": "sha512-yCcfVdiBFngVz9/keHin9EnsrQtQtEu3nRykNy9RVp+FiPFFbPJ3Sg6Qg4+TkmH0vMP5qsTKgXSsk80HRwvdgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "0.x", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true + }, "node_modules/ts-object-utils": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz", @@ -20955,7 +22551,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -21009,9 +22605,10 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz", + "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==", + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -21158,6 +22755,15 @@ "react": ">=16.8" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peer": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -21191,11 +22797,17 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -21721,6 +23333,15 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/occupi-mobile4/package.json b/frontend/occupi-mobile4/package.json index 22fb28d6..6d06f2b3 100644 --- a/frontend/occupi-mobile4/package.json +++ b/frontend/occupi-mobile4/package.json @@ -17,6 +17,12 @@ "transformIgnorePatterns": [ "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|@gluestack-ui/themed)" ], + "moduleNameMapper": { + "^@/(.*)$": "/$1" + }, + "setupFilesAfterEnv": [ + "@testing-library/jest-native/extend-expect" + ], "collectCoverage": true, "coverageReporters": ["lcov", "text"], "coverageDirectory": "coverage" @@ -37,6 +43,7 @@ "@ui-kitten/components": "^5.3.1", "axios": "^1.7.2", "date-fns": "^3.6.0", + "dotenv": "^16.4.5", "expo": "~51.0.14", "expo-blur": "^13.0.2", "expo-constants": "~16.0.2", @@ -82,7 +89,9 @@ }, "devDependencies": { "@babel/core": "^7.20.0", + "@types/axios": "^0.14.0", "@types/jest": "^29.5.12", + "@types/node": "^22.0.0", "@types/react": "~18.2.45", "@types/react-test-renderer": "^18.0.7", "eslint": "^8.57.0", @@ -90,6 +99,8 @@ "jest": "^29.7.0", "jest-expo": "~51.0.1", "react-test-renderer": "18.2.0", + "ts-jest": "^29.2.3", + "ts-node": "^10.9.2", "typescript": "~5.3.3" }, "private": true diff --git a/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx b/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx index f3eb6fe7..2df4c26f 100644 --- a/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx +++ b/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx @@ -13,6 +13,7 @@ import Navbar from '../../components/NavBar'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import * as SecureStore from 'expo-secure-store'; import { Skeleton } from 'moti/skeleton'; +import { useTheme } from '@/components/ThemeContext'; const groupDataInPairs = (data) => { if (!data) return []; @@ -36,9 +37,11 @@ interface Room { const BookRoom = () => { const router = useRouter(); - const colorScheme = useColorScheme(); + const { theme } = useTheme(); + const colorscheme = useColorScheme(); const toast = useToast(); - const [isDarkMode, setIsDarkMode] = useState(colorScheme === 'dark'); + const currentTheme = theme === "system" ? colorscheme : theme; + const isDarkMode = currentTheme === "dark"; const [layout, setLayout] = useState("row"); const [loading, setLoading] = useState(true); const [roomData, setRoomData] = useState([]); @@ -103,11 +106,8 @@ const BookRoom = () => { } }; fetchAllRooms(); - }, [toast, apiUrl, viewroomsendpoint]); + }, [toast]); - useEffect(() => { - setIsDarkMode(colorScheme === 'dark'); - }, [colorScheme]); const backgroundColor = isDarkMode ? 'black' : 'white'; const textColor = isDarkMode ? 'white' : 'black'; diff --git a/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx b/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx index da8bb457..dabed24a 100644 --- a/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx +++ b/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx @@ -24,177 +24,76 @@ import { } from 'react-native-responsive-screen'; import PagerView from 'react-native-pager-view'; import { useRouter } from 'expo-router'; - -interface Room { - _id: string; - roomName: string; - roomId: string; - roomNo: number; - floorNo: number; - minOccupancy: number; - maxOccupancy: number; - description: string; - emails: string[]; - date: string; - start: string; - end: string; - creator: string; -} - -const ViewBookingDetails = (bookingId:string, roomName:string) => { - const colorScheme = useColorScheme(); - const isDarkMode = colorScheme === 'dark'; - const [room, setRoom] = useState({}); +import { Booking } from '@/models/data'; +import { userCancelBooking, userCheckin } from '@/utils/bookings'; +import { useTheme } from '@/components/ThemeContext'; + +const ViewBookingDetails = () => { + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const isDarkMode = currentTheme === 'dark'; + const [room, setRoom] = useState(); const router = useRouter(); const [checkedIn, setCheckedIn] = useState(false); const [isLoading, setIsLoading] = useState(false); const toast = useToast(); - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const checkinendpoint = process.env.EXPO_PUBLIC_CHECK_IN; - const cancelbookingendpoint = process.env.EXPO_PUBLIC_CANCEL_BOOKING; // console.log("HERE:" + room); useEffect(() => { const getCurrentRoom = async () => { - let result : string = await SecureStore.getItemAsync('CurrentRoom'); - // console.log("CurrentRoom:",result); - // setUserDetails(JSON.parse(result).data); - let jsonresult = JSON.parse(result); - console.log(jsonresult); - setRoom(jsonresult); - setCheckedIn(jsonresult.checkedIn); + let result: string = await SecureStore.getItemAsync('CurrentRoom'); + // console.log("CurrentRoom:",result); + // setUserDetails(JSON.parse(result).data); + let jsonresult = JSON.parse(result); + // console.log(jsonresult); + setRoom(jsonresult); + setCheckedIn(jsonresult.checkedIn); }; getCurrentRoom(); - }, []); + }, []); + + // console.log("Room",room?._id); - // console.log("Room",room._id); - const checkin = async () => { - const body = { - "bookingId": room._id, - "creator": room.creator - }; setIsLoading(true); - console.log(body); - // console.log(apiUrl+""+checkinendpoint); - let authToken = await SecureStore.getItemAsync('Token'); - try { - const response = await fetch(`${apiUrl}${checkinendpoint}`, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - body: JSON.stringify(body), - credentials: "include" - }); - const data = await response.json(); - // console.log(data); - // const cookies = response.headers.get('Accept'); - // console.log(cookies); - if (response.ok) { - setCheckedIn(true); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - setIsLoading(false); - } else { - setIsLoading(false); - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); + const response = await userCheckin(); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); } - } catch (error) { - console.error('Error:', error); - } + }); + setIsLoading(false); }; const cancelBooking = async () => { - const body = { - "bookingId": room._id, - "creator": room.creator, - "roomId": room.roomId, - "emails": room.emails, - "roomName": room.roomName, - "floorNo": room.floorNo, - "date": room.date, - "start": room.start, - "end": room.end - - }; setIsLoading(true); - console.log(body); - let authToken = await SecureStore.getItemAsync('Token'); - try { - const response = await fetch(`${apiUrl}${cancelbookingendpoint}`, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - body: JSON.stringify(body), - credentials: "include" - }); - const data = await response.json(); - console.log(data); - // const cookies = response.headers.get('Accept'); - // console.log(cookies); - if (response.ok) { - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - setIsLoading(false); - router.replace("/home"); - } else { - setIsLoading(false); - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); + const response = await userCancelBooking(); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); } - } catch (error) { - console.error('Error:', error); - } + }); + setIsLoading(false); }; return ( - + - router.back()} /> - {room.roomName} + router.back()} /> + {room?.roomName} @@ -211,25 +110,25 @@ const ViewBookingDetails = (bookingId:string, roomName:string) => { - {room.roomName} + {room?.roomName} Fast OLED 3 - 5 - Floor: {room.floorNo === 0 ? 'G' : room.floorNo} + Floor: {room?.floorNo === 0 ? 'G' : room?.floorNo} - Attendees: {room.emails?.length} + Attendees: {room?.emails?.length} - {room.emails?.map((email, idx) => ( + {room?.emails?.map((email, idx) => ( {idx + 1}. {email} ))} - Description - The {room.roomName} is a state-of-the-art conference space designed for modern digital connectivity, seating 3-6 comfortably. Equipped with multiple HDMI ports, a high-definition projector or large LED screen, surround sound, and wireless display options, it ensures seamless presentations and video conferencing. The room features an intuitive control panel, high-speed Wi-Fi, and ample power outlets. Additional amenities include whiteboards, flip charts, adjustable lighting, and climate control, all within a professional and comfortable interior designed for productivity. + Description + The {room?.roomName} is a state-of-the-art conference space designed for modern digital connectivity, seating 3-6 comfortably. Equipped with multiple HDMI ports, a high-definition projector or large LED screen, surround sound, and wireless display options, it ensures seamless presentations and video conferencing. The room features an intuitive control panel, high-speed Wi-Fi, and ample power outlets. Additional amenities include whiteboards, flip charts, adjustable lighting, and climate control, all within a professional and comfortable interior designed for productivity. diff --git a/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx b/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx index b140e8ef..caa3e01c 100644 --- a/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx +++ b/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx @@ -14,30 +14,20 @@ import Navbar from '../../components/NavBar'; import * as SecureStore from 'expo-secure-store'; import { useRouter } from 'expo-router'; import { Skeleton } from 'moti/skeleton'; +import { Booking } from '@/models/data'; +import { fetchUserBookings } from '@/utils/bookings'; +import { useTheme } from '@/components/ThemeContext'; + + const groupDataInPairs = (data) => { const pairs = []; - for (let i = 0; i < data.length; i += 2) { - pairs.push(data.slice(i, i + 2)); + for (let i = 0; i < 10; i += 2) { + pairs.push(data?.slice(i, i + 2)); } return pairs; }; -interface Room { - _id: string; - roomName: string; - roomId: string; - roomNo: number; - floorNo: number; - minOccupancy: number; - maxOccupancy: number; - description: string; - emails: string[]; - date: string; - start: string; - end: string; -} - function extractTimeFromDate(dateString: string): string { const date = new Date(dateString); date.setHours(date.getHours() - 2); @@ -50,18 +40,33 @@ function extractDateFromDate(dateString: string): string { } const ViewBookings = () => { - const colorScheme = useColorScheme(); - const [isDarkMode, setIsDarkMode] = useState(colorScheme === 'dark'); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const isDarkMode = currentTheme === "dark"; const [layout, setLayout] = useState("row"); - const toast = useToast(); - const [roomData, setRoomData] = useState([]); + const [roomData, setRoomData] = useState(); // const [selectedSort, setSelectedSort] = useState("newest"); - const [email, setEmail] = useState(''); const router = useRouter(); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const viewbookingsendpoint = process.env.EXPO_PUBLIC_VIEW_BOOKINGS; + useEffect(() => { + const getRoomData = async () => { + try { + const roomData = await fetchUserBookings(); + if (roomData) { + // console.log(roomData); + setRoomData(roomData); + } else { + setRoomData([]); + } + } catch (error) { + console.error('Error fetching bookings:', error); + } + setLoading(false); + }; + getRoomData(); + }, []); const [accentColour, setAccentColour] = useState('greenyellow'); useEffect(() => { @@ -74,138 +79,36 @@ const ViewBookings = () => { const onRefresh = React.useCallback(() => { - const fetchAllRooms = async () => { - console.log("heree"); - let authToken = await SecureStore.getItemAsync('Token'); - console.log("Token:" + authToken); + const getRoomData = async () => { try { - const response = await fetch(`${apiUrl}${viewbookingsendpoint}?email=${email}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - }); - const data = await response.json(); - if (response.ok) { - setRoomData(data.data || []); // Ensure data is an array - setLoading(false); + const roomData = await fetchUserBookings(); + if (roomData) { + // console.log(roomData); + setRoomData(roomData); } else { - console.log(data); - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); + setRoomData([]); // Default value if no username is found } } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error: {error.message} - - ); - }, - }); + console.error('Error fetching bookings:', error); } + setLoading(false); }; setRefreshing(true); setTimeout(() => { setRefreshing(false); - fetchAllRooms(); + getRoomData(); }, 2000); - }, [toast, apiUrl, viewbookingsendpoint, email]); + }, []); const toggleLayout = () => { setLayout((prevLayout) => (prevLayout === "row" ? "grid" : "row")); }; - useEffect(() => { - setIsDarkMode(colorScheme === 'dark'); - }, [colorScheme]); + const backgroundColor = isDarkMode ? 'black' : 'white'; const textColor = isDarkMode ? 'white' : 'black'; const cardBackgroundColor = isDarkMode ? '#2C2C2E' : '#F3F3F3'; - // const data = [ - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // { title: 'HDMI Room', description: 'Boasting sunset views, long desks, and comfy chairs', Date: '17/06/2024', Time: '07:30-09:30', available: true }, - // ]; - useEffect(() => { - const fetchAllRooms = async () => { - let authToken = await SecureStore.getItemAsync('Token'); - let result = await SecureStore.getItemAsync('UserData'); - // console.log(result); - // if (result !== undefined) { - let jsonresult = JSON.parse(result); - setEmail(jsonresult?.data?.email); - // } - // console.log("Token:"+authToken); - // console.log("heree"); - try { - // console.log(`${apiUrl}${viewbookingsendpoint}?email=${jsonresult?.data?.email}`); - const response = await fetch(`${apiUrl}${viewbookingsendpoint}?email=${jsonresult?.data?.email}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - }); - const data = await response.json(); - // console.log(data); - if (response.ok) { - setRoomData(data.data || []); // Ensure data is an array - setLoading(false); - } else { - console.log(data); - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error: {error.message} - - ); - }, - }); - } - }; - fetchAllRooms(); - }, [toast, apiUrl, email, viewbookingsendpoint]); + const roomPairs = groupDataInPairs(roomData); @@ -313,8 +216,9 @@ const ViewBookings = () => { marginBottom: 20, }} > - {pair.map((room) => ( + {pair.map((room, idx) => ( handleRoomClick(JSON.stringify(room))} style={{ flex: 1, @@ -341,7 +245,7 @@ const ViewBookings = () => { {room.roomName} - Attendees: {room.emails.length} + Attendees: {room.emails?.length} Your booking time: @@ -366,8 +270,9 @@ const ViewBookings = () => { } > - {roomData.map((room) => ( + {roomData?.map((room, idx) => ( handleRoomClick(JSON.stringify(room))} style={{ flex: 1, @@ -398,7 +303,7 @@ const ViewBookings = () => { > {room.roomName} - Attendees: {room.emails.length} + Attendees: {room.emails?.length} Your booking time: diff --git a/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx b/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx index ff7983d5..7732ed84 100644 --- a/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx +++ b/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { StatusBar, useColorScheme, Dimensions } from 'react-native'; +import { StatusBar, useColorScheme, Dimensions, TouchableOpacity } from 'react-native'; import Navbar from '../../components/NavBar'; import { Text, @@ -11,14 +11,19 @@ import { ToastTitle, Button, ButtonText, + ScrollView, } from '@gluestack-ui/themed'; import { LineChart } from "react-native-chart-kit"; -import { FontAwesome6 } from '@expo/vector-icons'; import * as SecureStore from 'expo-secure-store'; +import { FontAwesome6 } from '@expo/vector-icons'; // import { router } from 'expo-router'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; +import { fetchUsername } from '@/utils/user'; +import { Booking } from '@/models/data'; +import { fetchUserBookings } from '@/utils/bookings'; +import { useTheme } from '@/components/ThemeContext'; // import { number } from 'zod'; const getRandomNumber = () => { @@ -27,12 +32,16 @@ const getRandomNumber = () => { const Dashboard = () => { const colorScheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorScheme : theme; const [numbers, setNumbers] = useState(Array.from({ length: 15 }, getRandomNumber)); - const [isDarkMode, setIsDarkMode] = useState(colorScheme === 'dark'); + const [isDarkMode, setIsDarkMode] = useState(currentTheme === 'dark'); const [checkedIn, setCheckedIn] = useState(false); - const [name, setName] = useState("User"); - const toast = useToast() - const [accentColour, setAccentColour] = useState('greenyellow'); + const [roomData, setRoomData] = useState({}); + const [username, setUsername] = useState(''); + const toast = useToast(); + // console.log(currentTheme); + // console.log(isDarkMode); useEffect(() => { const getAccentColour = async () => { @@ -41,127 +50,53 @@ const Dashboard = () => { }; getAccentColour(); }, []); - useEffect(() => { - const intervalId = setInterval(() => { - setNumbers(prevNumbers => { - const newNumbers = [getRandomNumber(), ...prevNumbers.slice(0, 14)]; - return newNumbers; - }); - }, 3000); - setIsDarkMode(colorScheme === 'dark'); - return () => clearInterval(intervalId); - }, [colorScheme]); useEffect(() => { - const getUserDetails = async () => { - let result = await SecureStore.getItemAsync('UserData'); - console.log(result); - if (result !== undefined) { - let jsonresult = JSON.parse(result); - setName(String(jsonresult?.data?.details?.name)); - } - }; - const getUserSettings = async () => { + const getUsername = async () => { try { - let authToken = await SecureStore.getItemAsync('Token'); - let email = await SecureStore.getItemAsync('Email'); - // console.log(authToken); - const response = await fetch(`https://dev.occupi.tech/api/get-notification-settings?email=${email}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - console.log(data); - if (response.ok) { - const settings = { - invites: data.data.invites, - bookingReminder: data.data.bookingReminder - }; - // console.log(settings); - await SecureStore.setItemAsync('Notifications', JSON.stringify(settings)); + const name = await fetchUsername(); + // console.log(name); + if (name) { + setUsername(name); } else { - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); + setUsername('Guest'); // Default value if no username is found } } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error - - ); - }, - }); + console.error('Error fetching username:', error); + setUsername('Guest'); // Default in case of an error } + }; + + const getRoomData = async () => { try { - let authToken = await SecureStore.getItemAsync('Token'); - let email = await SecureStore.getItemAsync('Email'); - // console.log(authToken); - const response = await fetch(`https://dev.occupi.tech/api/get-security-settings?email=${email}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - console.log(data); - if (response.ok) { - const settings = { - mfa: data.data.mfa, - forcelogout: data.data.forceLogout - }; - console.log(settings); - await SecureStore.setItemAsync('Security', JSON.stringify(settings)); - } else { - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); + const roomData = await fetchUserBookings(); + if (roomData) { + // console.log(roomData); + setRoomData(roomData[0]); + // console.log(roomData[0]); } } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error - - ); - }, - }); + console.error('Error fetching bookings:', error); } - } - getUserSettings(); - getUserDetails(); - }, [toast]); + }; + getRoomData(); + getUsername(); + }, []); + + + const [accentColour, setAccentColour] = useState('greenyellow'); + + + useEffect(() => { + const intervalId = setInterval(() => { + setNumbers(prevNumbers => { + const newNumbers = [getRandomNumber(), ...prevNumbers.slice(0, 14)]; + return newNumbers; + }); + }, 3000); + setIsDarkMode(currentTheme === 'dark'); + return () => clearInterval(intervalId); + }, [currentTheme]); const checkIn = () => { if (checkedIn === false) { @@ -187,125 +122,174 @@ const Dashboard = () => { } }; - // async function saveUserEmail(value) { - // await SecureStore.setItemAsync('email', value); - // } - - - // saveUserEmail('kamogelomoeketse@gmail.com'); + function extractTimeFromDate(dateString: string): string { + const date = new Date(dateString); + date.setHours(date.getHours() - 2); + return date.toTimeString().substring(0, 5); + } + function extractDateFromDate(dateString: string): string { + const date = new Date(dateString); + return date.toDateString(); + } const backgroundColor = isDarkMode ? '#1C1C1E' : 'white'; const textColor = isDarkMode ? 'white' : 'black'; const cardBackgroundColor = isDarkMode ? '#2C2C2E' : '#F3F3F3'; return ( - - - - - - Hi {name} πŸ‘‹ - - - Welcome to Occupi - + <> + + + + + + Hi {username} πŸ‘‹ + + + Welcome back to Occupi + + + logo - logo - - - - - {numbers[0]} - - {numbers[0] / 10 + 5}% + + Next booking: + + + image + + {roomData.roomName} + + + + {extractDateFromDate(roomData.date)} + {extractTimeFromDate(roomData.start)}-{extractTimeFromDate(roomData.end)} + + + - - - - - {checkedIn ? ( - - ) : ( - - )} - - {/* + + + Capacity {numbers[0] / 10 + 5}% + {numbers[0]} + Compared to + Yesterday + {/* + {numbers[0] / 10 + 5}% + */} + + + + + {checkedIn ? ( + + ) : ( + + )} + + {/* logo */} - - Occupancy levels - + Occupancy levels + `rgba(0, 0, 0, ${opacity})`, + labelColor: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`, + style: { + borderRadius: 20 + }, + propsForDots: { + r: "0", + strokeWidth: "2", + stroke: "green" } - ] - }} - width={Dimensions.get("window").width - 30} // from react-native - height={220} - // yAxisLabel="" - // yAxisSuffix="k" - yAxisInterval={1} // optional, defaults to 1 - chartConfig={{ - backgroundColor: "white", - backgroundGradientFrom: "yellowgreen", - backgroundGradientTo: "cyan", - decimalPlaces: 0, // optional, defaults to 2dp - color: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`, - labelColor: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`, - style: { - borderRadius: 20 - }, - propsForDots: { - r: "0", - strokeWidth: "2", - stroke: "green" - } - }} - bezier - style={{ - marginVertical: 8, - borderRadius: 16, - }} - /> - + }} + bezier + style={{ + marginVertical: 8, + borderRadius: 16, + }} + /> + + - + ); }; diff --git a/frontend/occupi-mobile4/screens/Dashboard/__tests__/Dashboard-test.tsx b/frontend/occupi-mobile4/screens/Dashboard/__tests__/Dashboard-test.tsx index ae402883..fe7814d5 100644 --- a/frontend/occupi-mobile4/screens/Dashboard/__tests__/Dashboard-test.tsx +++ b/frontend/occupi-mobile4/screens/Dashboard/__tests__/Dashboard-test.tsx @@ -2,9 +2,27 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react-native'; import { StyledProvider, Theme } from '@gluestack-ui/themed'; import Dashboard from '../Dashboard'; +import { useNavBar } from '../../../components/NavBarProvider'; // Adjust the path as needed jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper'); // To prevent warnings about Animated module +jest.mock('react-native/Libraries/Settings/Settings', () => ({ + get: jest.fn(), + set: jest.fn(), +})); + +jest.mock('react-native', () => ({ + ...jest.requireActual('react-native'), + useColorScheme: () => 'light', +})); + +jest.mock('../../../components/NavBarProvider', () => ({ + useNavBar: () => ({ + currentTab: 'Dashboard', + setCurrentTab: jest.fn(), + }), +})); + jest.mock('@gluestack-ui/themed', () => ({ ...jest.requireActual('@gluestack-ui/themed'), useToast: () => ({ @@ -47,8 +65,8 @@ describe('Dashboard component', () => { // Mock SecureStore getItemAsync to resolve with the mocked data require('expo-secure-store').getItemAsync.mockResolvedValueOnce(JSON.stringify(mockedData)); - it('renders text correctly', () => { - const { getByText } = renderWithProvider(); + it('renders text correctly', async () => { + const { getByText } = await renderWithProvider(); expect(getByText('Welcome to Occupi')).toBeTruthy(); }); diff --git a/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx b/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx index c02b09ca..5dcf5414 100644 --- a/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx +++ b/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx @@ -2,20 +2,21 @@ import React, { useRef, useState, useEffect } from 'react'; import { View, StyleSheet, TextInput } from 'react-native'; import { VStack, Box, HStack, Image, Heading, Toast, useToast, ToastTitle, Text, } from '@gluestack-ui/themed'; // import { useForm } from 'react-hook-form'; -// import { z } from 'zod'; +import { z } from 'zod'; // import { zodResolver } from '@hookform/resolvers/zod'; import * as SecureStore from 'expo-secure-store'; -import { useRouter, useLocalSearchParams } from 'expo-router'; +import { useRouter, useLocalSearchParams } from 'expo-router'; import Logo from './assets/images/Occupi/file.png'; import StyledExpoRouterLink from '@/components/StyledExpoRouterLink'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import { LinearGradient } from 'expo-linear-gradient'; +import { VerifyUserOtpLogin, verifyUserOtpRegister } from '@/utils/auth'; -// const OTPSchema = z.object({ -// OTP: z.string().min(6, 'OTP must be at least 6 characters in length'), -// }); +const OTPSchema = z.object({ + OTP: z.string().min(6, 'OTP must be at least 6 characters in length'), +}); -// type OTPSchemaType = z.infer; +type OTPSchemaType = z.infer; const OTPVerification = () => { const [email, setEmail] = useState(""); @@ -24,12 +25,9 @@ const OTPVerification = () => { const otpSent = useState(false); const timerRef = useRef(null); const [loading, setLoading] = useState(false); - const router = useRouter(); const toast = useToast(); const [otp, setOtp] = useState(['', '', '', '', '', '']); - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const getUserDetailsUrl = process.env.EXPO_PUBLIC_GET_USER_DETAILS; - // console.log(email); + const [state, setState] = useState(); useEffect(() => { if (remainingTime > 0 && !otpSent) { @@ -49,20 +47,13 @@ const OTPVerification = () => { useEffect(() => { const getUserEmail = async () => { let email = await SecureStore.getItemAsync('Email'); - // console.log("email",email); + const state = await SecureStore.getItemAsync('AppState'); + setState(state); setEmail(email); }; getUserEmail(); }, []); - async function storeToken(value) { - await SecureStore.setItemAsync('Token', value); - } - - async function storeUserData(value) { - await SecureStore.setItemAsync('UserData', value); - } - // console.log("here",email); const onSubmit = async () => { @@ -74,99 +65,33 @@ const OTPVerification = () => { // return; // } // setValidationError(null); - console.log(pin); setLoading(true); - try { - const response = await fetch('https://dev.occupi.tech/auth/verify-otp-mobile-login', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email: email, - otp: pin - }), - credentials: "include" + if (state === 'verify_otp_register') { + const response = await verifyUserOtpRegister(email, pin); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); + } + }); + } + else { + const response = await VerifyUserOtpLogin(email, pin); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); + } }); - const data = await response.json(); - if (response.ok) { - setLoading(false); - // console.log(data.data.token); - storeToken(data.data.token); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - try { - let authToken = await SecureStore.getItemAsync('Token'); - console.log(authToken); - const response = await fetch(`${apiUrl}${getUserDetailsUrl}?email=${email}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - // console.log("here"); - if (response.ok) { - storeUserData(JSON.stringify(data)); - // console.log(`Data of ${email}: `, data); - } else { - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error - - ); - }, - }); - } - router.replace('/home'); - } else { - setLoading(false); - // console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); - // setResponse('An error occurred'); } - // }, 3000); setLoading(false); }; @@ -185,41 +110,41 @@ const OTPVerification = () => { ); return ( - - - - + + + + Entered OTP: {otp.join('')} - {remainingTime} seconds remaining - {loading ? ( - - ) : ( - {remainingTime} seconds remaining + {loading ? ( + - )} - - - - - + text="Verifying OTP..." + /> + ) : ( + + )} + + + + + ); }; @@ -335,7 +260,7 @@ const OTPInput = ({ otp, setOtp }) => { keyboardType="numeric" maxLength={1} ref={(ref) => inputRefs.current[index] = ref} - // autoFocus={index === inputRefs.current[index]} // Auto focus the first input on mount + // autoFocus={index === inputRefs.current[index]} // Auto focus the first input on mount /> ))} diff --git a/frontend/occupi-mobile4/screens/Login/SetDetails.tsx b/frontend/occupi-mobile4/screens/Login/SetDetails.tsx index 53101689..007a9460 100644 --- a/frontend/occupi-mobile4/screens/Login/SetDetails.tsx +++ b/frontend/occupi-mobile4/screens/Login/SetDetails.tsx @@ -27,6 +27,8 @@ import { useColorScheme } from 'react-native'; import { heightPercentageToDP as hp } from 'react-native-responsive-screen'; import GradientButton from '@/components/GradientButton'; import LoadingGradientButton from '@/components/LoadingGradientButton'; +import { updateDetails } from '@/utils/user'; +import { extractDateFromTimestamp } from '@/utils/utils'; const COLORS = { white: '#FFFFFF', @@ -47,20 +49,14 @@ const SIZES = { }; const SetDetails = () => { - const [selectedGenderIndex, setSelectedGenderIndex] = useState(1); + const [selectedGenderIndex, setSelectedGenderIndex] = useState(''); const [name, setName] = useState(''); - const [email, setEmail] = useState(''); - const [employeeId, setEmployeeId] = useState(''); const [phoneNumber, setPhoneNumber] = useState(''); const [pronouns, setPronouns] = useState(''); const [date, setDate] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isDatePickerVisible, setDatePickerVisibility] = useState(false); let colorScheme = 'light'; - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const getUserDetailsUrl= process.env.EXPO_PUBLIC_GET_USER_DETAILS; - const updateDetailsUrl = process.env.EXPO_PUBLIC_UPDATE_USER_DETAILS; - console.log(apiUrl, getUserDetailsUrl, updateDetailsUrl); const showDatePicker = () => { setDatePickerVisibility(true); @@ -70,78 +66,18 @@ const SetDetails = () => { setDatePickerVisibility(false); }; - const handleConfirm = (selectedDate) => { - setDate(selectedDate); + const handleConfirm = (selectedDate: string) => { + console.log('selected',extractDateFromTimestamp(selectedDate)); + setDate(extractDateFromTimestamp(selectedDate)); hideDatePicker(); }; const onSave = async () => { - const body = { - "email": email, - "details": { - "contactNo": phoneNumber, - "gender": "Male", - "name": name, - "pronouns": pronouns - } - }; - // console.log(JSON.stringify(body)); setIsLoading(true); - try { - let authToken = await SecureStore.getItemAsync('Token'); - const response = await fetch(`${apiUrl}${updateDetailsUrl}`, { - method: 'PUT', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - body: JSON.stringify(body), - credentials: "include" - }); - const data = await response.json(); - console.log(data); - if (response.ok) { - console.log(response); - setIsLoading(false); - alert('Details updated successfully'); - } else { - console.log(data); - setIsLoading(false); - } - } catch (error) { - setIsLoading(false); - console.error('Error:', error); - // setResponse('An error occurred'); - } - - try { - let authToken = await SecureStore.getItemAsync('Token'); - const response = await fetch(`${apiUrl}${getUserDetailsUrl}?email=${email}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - if (response.ok) { - saveUserData(JSON.stringify(data)); - console.log(data); - } else { - console.log(data); - } - } catch (error) { - console.error('Error:', error); - } + const response = await updateDetails(name,date,selectedGenderIndex,phoneNumber,pronouns) + console.log(response); + setIsLoading(false); }; - console.log(selectedGenderIndex); - - async function saveUserData(value) { - await SecureStore.setItemAsync('UserData', value); - } return ( { > - router.replace('/settings')} - /> - Account Details + My account { style={styles.icon} /> - Full name { /> Gender - setSelectedGenderIndex(index)}> + setSelectedGenderIndex(index)}> { borderColor="#f2f2f2" h={hp('5%')} px="$4" + > Male @@ -237,7 +166,6 @@ const SetDetails = () => { - Cell No { ) : ( ) } - diff --git a/frontend/occupi-mobile4/screens/Login/SignIn.tsx b/frontend/occupi-mobile4/screens/Login/SignIn.tsx index 231f4803..d938634b 100644 --- a/frontend/occupi-mobile4/screens/Login/SignIn.tsx +++ b/frontend/occupi-mobile4/screens/Login/SignIn.tsx @@ -37,10 +37,10 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { AlertTriangle, EyeIcon, EyeOffIcon } from 'lucide-react-native'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; -import * as SecureStore from 'expo-secure-store'; import Logo from '../../screens/Login/assets/images/Occupi/file.png'; import StyledExpoRouterLink from '../../components/StyledExpoRouterLink'; import GradientButton from '@/components/GradientButton'; +import { UserLogin } from '@/utils/auth'; const signInSchema = z.object({ email: z.string().min(1, 'Email is required').email(), @@ -67,9 +67,6 @@ const SignInForm = () => { } = useForm({ resolver: zodResolver(signInSchema), }); - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const loginUrl = process.env.EXPO_PUBLIC_LOGIN; - const getUserDetailsUrl = process.env.EXPO_PUBLIC_GET_USER_DETAILS; const isEmailFocused = useState(false); const [loading, setLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); @@ -87,19 +84,6 @@ const SignInForm = () => { // console.log('Biometric hardware available:', isBiometricAvailable); }; - async function storeUserData(value) { - await SecureStore.setItemAsync('UserData', value); - } - - async function storeToken(value) { - await SecureStore.setItemAsync('Token', value); - } - - async function storeUserEmail(value) { - await SecureStore.setItemAsync('Email', value); - } - - const handleBiometricSignIn = async () => { const biometricType = await LocalAuthentication.supportedAuthenticationTypesAsync(); console.log('Supported biometric types:', biometricType); @@ -114,7 +98,7 @@ const SignInForm = () => { }); console.log('Biometric authentication result:', result); if (result.success) { - router.replace('/home'); + // router.replace('/home'); } else { console.log('Biometric authentication failed'); toast.show({ @@ -160,104 +144,17 @@ const SignInForm = () => { const onSubmit = async (_data: SignInSchemaType) => { setLoading(true); - storeUserEmail(_data.email); - const body = { - email: _data.email, - password: _data.password - }; - console.log(body); - try { - const response = await fetch(`${apiUrl}${loginUrl}`, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(body), - credentials: "include" - }); - const data = await response.json(); - if (response.ok) { - console.log("Data here", data); - setLoading(false); - if (data.data) { - storeToken(data.data.token); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - try { - let authToken = await SecureStore.getItemAsync('Token'); - // console.log(authToken); - const response = await fetch(`${apiUrl}${getUserDetailsUrl}?email=${_data.email}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - console.log("here"); - if (response.ok) { - storeUserData(JSON.stringify(data)); - console.log(`Data of ${_data.email}: `, data); - } else { - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error - - ); - }, - }); - } - router.replace('/home'); - } - else { - setLoading(false); - router.replace('/verify-otp'); - } - } else { - setLoading(false); - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); + const response = await UserLogin(_data.email, _data.password); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); } - } catch (error) { - console.error('Error:', error); - } + }); setLoading(false); }; diff --git a/frontend/occupi-mobile4/screens/Login/SignUp.tsx b/frontend/occupi-mobile4/screens/Login/SignUp.tsx index 874d99ba..30e09d82 100644 --- a/frontend/occupi-mobile4/screens/Login/SignUp.tsx +++ b/frontend/occupi-mobile4/screens/Login/SignUp.tsx @@ -29,6 +29,7 @@ import { FormControlLabelText, View } from '@gluestack-ui/themed'; +import { retrievePushToken } from '@/utils/notifications'; import GradientButton from '@/components/GradientButton'; import { Controller, useForm } from 'react-hook-form'; import { AlertTriangle, EyeIcon, EyeOffIcon } from 'lucide-react-native'; @@ -39,6 +40,7 @@ import { Keyboard } from 'react-native'; import StyledExpoRouterLink from '../../components/StyledExpoRouterLink'; import { router } from 'expo-router'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; +import { userRegister } from '@/utils/auth'; const isEmployeeIdFocused = false; const signUpSchema = z.object({ @@ -69,6 +71,7 @@ const signUpSchema = z.object({ type SignUpSchemaType = z.infer; +retrievePushToken(); const SignUpForm = () => { const { @@ -85,50 +88,18 @@ const SignUpForm = () => { const onSubmit = async (_data: SignUpSchemaType) => { if (_data.password === _data.confirmpassword) { setLoading(true); - try { - const response = await fetch('https://dev.occupi.tech/auth/register', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email: _data.email, - password: _data.password - }), - credentials: "include" - }); - const data = await response.json(); - if (response.ok) { - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - router.push({pathname:'/verify-otp', params: { email: _data.email}}); - } else { - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); + const response = await userRegister(_data.email, _data.password, _data.employeeId); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); } - setLoading(false) + }); + setLoading(false); } else { toast.show({ placement: 'bottom right', diff --git a/frontend/occupi-mobile4/screens/Login/SplashScreen.tsx b/frontend/occupi-mobile4/screens/Login/SplashScreen.tsx index b0902ecc..3cb20b4f 100644 --- a/frontend/occupi-mobile4/screens/Login/SplashScreen.tsx +++ b/frontend/occupi-mobile4/screens/Login/SplashScreen.tsx @@ -93,7 +93,7 @@ export default function SplashScreen() { useEffect(() => { const timer = setTimeout(() => { setSelectedIndex(1); // Assuming Onboarding1 is at index 1 - router.replace('/set-details'); // Navigate to Onboarding1 screen + router.replace('/welcome'); // Navigate to Onboarding1 screen }, 5000); // 8 seconds return () => clearTimeout(timer); // Clean up timer on component unmount diff --git a/frontend/occupi-mobile4/screens/Login/__tests__/Welcome-test.tsx b/frontend/occupi-mobile4/screens/Login/__tests__/Welcome-test.tsx index 704749b7..6192afb4 100644 --- a/frontend/occupi-mobile4/screens/Login/__tests__/Welcome-test.tsx +++ b/frontend/occupi-mobile4/screens/Login/__tests__/Welcome-test.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react-native'; import { StyledProvider, Theme } from '@gluestack-ui/themed'; -import Welcome from '../Welcome'; // Adjust the path to your component +import Welcome from '../Welcome'; +import GradientButton from '@/components/GradientButton'; jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper'); // To prevent warnings about Animated module jest.mock('expo-router', () => ({ @@ -11,6 +12,7 @@ jest.mock('expo-router', () => ({ }, })); + const renderWithProvider = (component) => { return render( diff --git a/frontend/occupi-mobile4/screens/Notifications/Notifications.tsx b/frontend/occupi-mobile4/screens/Notifications/Notifications.tsx index b663551c..fc21b983 100644 --- a/frontend/occupi-mobile4/screens/Notifications/Notifications.tsx +++ b/frontend/occupi-mobile4/screens/Notifications/Notifications.tsx @@ -14,11 +14,15 @@ import { StatusBar, useColorScheme, Dimensions } from 'react-native'; import { AntDesign, Entypo, FontAwesome6 } from '@expo/vector-icons'; import { Skeleton } from 'moti/skeleton'; import axios from 'axios'; +import { useTheme } from '@/components/ThemeContext'; +import { getUserNotifications } from '@/utils/notifications'; const Notifications = () => { - const colorScheme = useColorScheme(); - const toast = useToast(); - const [notifications, setNotifications] = useState(); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const [accentColour, setAccentColour] = useState('greenyellow'); + const currentTheme = theme === "system" ? colorscheme : theme; + const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(true); const todayNotifications = []; const yesterdayNotifications = []; @@ -26,17 +30,25 @@ const Notifications = () => { const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; + useEffect(() => { + const getSettings = async () => { + let accentcolour = await SecureStore.getItemAsync('accentColour'); + setAccentColour(accentcolour); + }; + getSettings(); + }, []); + const formatNotificationDate = (sendTime) => { const now = new Date(); // console.log(now); const notificationDate = new Date(sendTime); - console.log(notificationDate); + // console.log(notificationDate); const differenceInHours = Math.floor((now - notificationDate) / (1000 * 60 * 60)); const differenceInDays = Math.floor(differenceInHours / 24); if (differenceInDays === 0) { - console.log(differenceInDays); + // console.log(differenceInDays); return differenceInHours < 1 ? 'less than an hour ago' : `${differenceInHours} hours ago`; } else if (differenceInDays === 1) { return 'yesterday'; @@ -46,9 +58,10 @@ const Notifications = () => { }; if (notifications) { + console.log('yurpp'); notifications.forEach(notification => { const formattedDate = formatNotificationDate(notification.send_time); - + if (formattedDate.includes('hours ago') || formattedDate.includes('hour ago')) { todayNotifications.push(notification); } else if (formattedDate === 'yesterday') { @@ -62,67 +75,20 @@ const Notifications = () => { useEffect(() => { const getNotifications = async () => { - let userEmail = await SecureStore.getItemAsync('Email'); - let authToken = await SecureStore.getItemAsync('Token'); - - try { - const response = await axios.get('https://dev.occupi.tech/api/get-notifications', { - params: { - filter: { - emails: [{ userEmail }] - }, - order_desc: "send_time" - }, - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - withCredentials: true - }); - const data = response.data; - // console.log(`Response Data: ${JSON.stringify(data.data)}`); - console.log(data); - if (response.status === 200) { - setNotifications(data.data || []); // Ensure data is an array - setLoading(false); - } else { - console.log(data); - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.error.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Network Error: {error.message} - - ); - }, - }); - } + const notifications = await getUserNotifications(); + // console.log(notifications); + setNotifications(notifications); + setLoading(false); }; getNotifications(); - }, [apiUrl, toast]) + }, []) const renderNotifications = (notificationList) => ( notificationList.map((notification, idx) => ( - - + + {notification.message} Β· {formatNotificationDate(notification.send_time)} @@ -132,10 +98,10 @@ const Notifications = () => { return ( - + - Notifications - + Notifications + @@ -143,20 +109,20 @@ const Notifications = () => { <> {Array.from({ length: 8 }, (_, index) => ( - + ))} ) : ( - Recent + Recent {renderNotifications(todayNotifications)} - Yesterday + Yesterday {renderNotifications(yesterdayNotifications)} - Older + Older {renderNotifications(olderNotifications)} diff --git a/frontend/occupi-mobile4/screens/Office/BookingDetails.tsx b/frontend/occupi-mobile4/screens/Office/BookingDetails.tsx index e978bd23..207064d7 100644 --- a/frontend/occupi-mobile4/screens/Office/BookingDetails.tsx +++ b/frontend/occupi-mobile4/screens/Office/BookingDetails.tsx @@ -26,8 +26,9 @@ import { ActivityIndicator } from 'react-native'; import * as LocalAuthentication from "expo-local-authentication"; import * as SecureStore from 'expo-secure-store'; import GradientButton from '@/components/GradientButton'; - -import { sendPushNotification } from "@/utils/utils"; +import { sendPushNotification } from "@/utils/notifications"; +import { userBookRoom } from "@/utils/bookings"; +import { useTheme } from "@/components/ThemeContext"; const BookingDetails = () => { const navigation = useNavigation(); @@ -35,23 +36,19 @@ const BookingDetails = () => { const [email, setEmail] = useState(""); const [loading, setLoading] = useState(false); const [bookingInfo, setbookingInfo] = useState(); - const colorScheme = useColorScheme(); + const colorscheme = useColorScheme(); const toast = useToast(); const router = useRouter(); - const [creatorEmail, setCreatorEmail] = useState(''); const [startTime, setStartTime] = useState(''); const [endTime, setEndTime] = useState(''); - const isDark = colorScheme === "dark"; + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const isDark = colorscheme === "dark"; const [pushTokens, setPushTokens] = useState([]); - // console.log(creatorEmail + roomId + floorNo); - // console.log(bookingInfo?); - // console.log(startTime); const [attendees, setAttendees] = useState(['']); // console.log(attendees); const cardBackgroundColor = isDark ? '#2C2C2E' : '#F3F3F3'; const steps = ["Booking details", "Invite attendees", "Receipt"]; - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const bookroomendpoint = process.env.EXPO_PUBLIC_BOOK_ROOM; const [accentColour, setAccentColour] = useState('greenyellow'); useEffect(() => { @@ -64,21 +61,13 @@ const BookingDetails = () => { useEffect(() => { const getbookingInfo = async () => { - let userinfo = await SecureStore.getItemAsync('UserData'); - // if (result !== undefined) { - let jsoninfo = JSON.parse(userinfo); - console.log("data", jsoninfo?.data.details.name); - setCreatorEmail(jsoninfo?.data?.email); + let userEmail = await SecureStore.getItemAsync('Email'); let result: string = await SecureStore.getItemAsync('BookingInfo'); - console.log("CurrentRoom:", jsoninfo?.data?.email); - // setUserDetails(JSON.parse(result).data); let jsonresult = JSON.parse(result); - console.log("BookingInfo", jsonresult); setbookingInfo(jsonresult); setStartTime(jsonresult.startTime); setEndTime(jsonresult.endTime); - console.log(jsoninfo?.data?.email); - setAttendees([jsoninfo?.data?.email]); + setAttendees([userEmail]); }; getbookingInfo(); }, []); @@ -95,92 +84,23 @@ const BookingDetails = () => { }; const onSubmit = async () => { - const body = { - "roomId": bookingInfo?.roomId, - "emails": attendees, - "roomName": bookingInfo?.roomName, - "creator": creatorEmail, - "floorNo": bookingInfo?.floorNo, - "date": `${bookingInfo?.date}T00:00:00.000+00:00`, - "start": `${bookingInfo?.date}T${startTime}:00.000+00:00`, - "end": `${bookingInfo?.date}T${endTime}:00.000+00:00` - }; - console.log("hereeeeee", body); - let authToken = await SecureStore.getItemAsync('Token'); - let userinfo = await SecureStore.getItemAsync('UserData'); - let jsoninfo = JSON.parse(userinfo); - try { - setLoading(true); - const response = await fetch(`${apiUrl}${bookroomendpoint}`, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}`, - 'X-Timezone': 'Africa/Johannesburg' - }, - body: JSON.stringify(body), - credentials: "include" - }); - const data = await response.json(); - console.log(data); - console.log(attendees); - if (response.ok) { - try { - const response = await fetch(`${apiUrl}/api/get-push-tokens?emails=${attendees.slice(1)}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}`, - 'X-Timezone': 'Africa/Johannesburg' - }, - credentials: "include" - }); - const data = await response.json(); - console.log("PUSHH TOKENSS",data); - if (data.data) { - let tokens = data.data.map((item) => item.expoPushToken); - setPushTokens(tokens); - console.log(tokens); - sendPushNotification(tokens, "New Booking", `${jsoninfo?.data.details.name} has invited you to a booking.`); - } - setCurrentStep(2); - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - } catch (error) { - setLoading(false); - console.error('Error:', error); - } - } else { - console.log(data); - setLoading(false); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); + setLoading(true); + const response = await userBookRoom(attendees, startTime, endTime); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); } - } catch (error) { - setLoading(false); - console.error('Error:', error); - // setResponse('An error occurred'); + }); + + if (response === 'Successfully booked!') { + setCurrentStep(2); } - // }, 3000); + setLoading(false); }; const renderAttendee = ({ item }) => ( @@ -314,9 +234,9 @@ const BookingDetails = () => { }} > navigation.goBack()}> - router.back()} /> + router.back()} /> - {/* */} + {/* */} { @@ -61,8 +39,10 @@ const OfficeDetails = () => { const [startTime, setStartTime] = useState(); const [endTime, setEndTime] = useState(); const router = useRouter(); - const colorScheme = useColorScheme(); - const isDarkMode = colorScheme === 'dark'; + const { theme } = useTheme(); + const colorscheme = useColorScheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const isDarkMode = currentTheme === 'dark'; const [room, setRoom] = useState(); const navigation = useNavigation>(); const scrollX = useRef(new Animated.Value(0)).current; @@ -91,7 +71,7 @@ const OfficeDetails = () => { useEffect(() => { const getCurrentRoom = async () => { let result : string = await SecureStore.getItemAsync('CurrentRoom'); - console.log("CurrentRoom:",result); + // console.log("CurrentRoom:",result); // setUserDetails(JSON.parse(result).data); let jsonresult = JSON.parse(result); // console.log(jsonresult); @@ -134,27 +114,27 @@ const OfficeDetails = () => { maxOccupancy: room?.maxOccupancy }; - console.log(bookingInfo); + // console.log(bookingInfo); await SecureStore.setItemAsync('BookingInfo', JSON.stringify(bookingInfo)); router.replace('/booking-details'); } - + console.log(theme); // console.log(room?); // console.log(userEmail); return ( <> {/* Top Section */} - - navigation.goBack()} /> + + navigation.goBack()} /> {room?.roomName} - + diff --git a/frontend/occupi-mobile4/screens/Settings/Appearance.tsx b/frontend/occupi-mobile4/screens/Settings/Appearance.tsx index 07b95a46..dc6aee5e 100644 --- a/frontend/occupi-mobile4/screens/Settings/Appearance.tsx +++ b/frontend/occupi-mobile4/screens/Settings/Appearance.tsx @@ -3,7 +3,8 @@ import { StyleSheet, Alert, TextInput, - TouchableOpacity + TouchableOpacity, + useColorScheme } from 'react-native'; import { Feather } from '@expo/vector-icons'; import { MaterialCommunityIcons, FontAwesome } from '@expo/vector-icons'; @@ -15,10 +16,11 @@ import { Box } from '@gluestack-ui/themed'; import { router } from 'expo-router'; -import { useColorScheme, Switch } from 'react-native'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import GradientButton from '@/components/GradientButton'; import * as SecureStore from 'expo-secure-store'; +import { storeTheme, storeAccentColour } from '@/services/securestore'; +import { useTheme } from '@/components/ThemeContext'; const FONTS = { h3: { fontSize: 20, fontWeight: 'bold' }, @@ -32,30 +34,25 @@ const SIZES = { }; const Appearance = () => { - //retrieve user settings ad assign variables accordingly - const onSave = () => { - //integration here - }; const [accentColour, setAccentColour] = useState('greenyellow'); - const [theme, setTheme] = useState(''); - let colorScheme = theme; + const { theme, setTheme } = useTheme(); + const colorscheme = useColorScheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + + const onSave = () => { + storeAccentColour(accentColour); + storeTheme(theme); + router.back(); + } useEffect(() => { - const getAccentColour = async () => { + const getSettings = async () => { let accentcolour = await SecureStore.getItemAsync('accentColour'); setAccentColour(accentcolour); }; - getAccentColour(); + getSettings(); }, []); - console.log(accentColour); - - const setAccentcolour = async (value) => { - setAccentColour(value); - await SecureStore.setItemAsync('accentColour', value); - } - // setAccentcolour(); - const handleBack = () => { // if (isSaved === false) { // Alert.alert( @@ -76,44 +73,44 @@ const Appearance = () => { router.back(); // } } - console.log(theme); + // console.log(theme); return ( - + - + Appearance - Mode - + Mode + setTheme("light")} style={{ width: wp('25%') }}> white - Light + Light setTheme("dark")} style={{ width: wp('25%') }}> @@ -121,13 +118,13 @@ const Appearance = () => { white - Dark + Dark setTheme("system")} style={{ width: wp('25%') }}> @@ -135,90 +132,90 @@ const Appearance = () => { white - System + System - Accent colour - + Accent colour + - setAccentcolour("lightgrey")}> + setAccentColour("lightgrey")}> - + - setAccentcolour("#FF4343")}> + setAccentColour("#FF4343")}> - + - setAccentcolour("#FFB443")}> + setAccentColour("#FFB443")}> - + - setAccentcolour("greenyellow")}> + setAccentColour("greenyellow")}> - + - setAccentcolour("#43FF61")}> + setAccentColour("#43FF61")}> - + - setAccentcolour("#43F4FF")}> + setAccentColour("#43F4FF")}> - + - setAccentcolour("#4383FF")}> + setAccentColour("#4383FF")}> - + - setAccentcolour("#AC43FF")}> + setAccentColour("#AC43FF")}> - + - setAccentcolour("#FF43F7")}> + setAccentColour("#FF43F7")}> - + - setAccentcolour("purple")}> - setAccentcolour("#FF4343")}> - + setAccentColour("purple")}> + setAccentColour("#FF4343")}> + - Or enter a custom colour + Or enter a custom colour - Custom colour: + Custom colour: diff --git a/frontend/occupi-mobile4/screens/Settings/ChangePassword.tsx b/frontend/occupi-mobile4/screens/Settings/ChangePassword.tsx index 9c7a4be7..564e50a1 100644 --- a/frontend/occupi-mobile4/screens/Settings/ChangePassword.tsx +++ b/frontend/occupi-mobile4/screens/Settings/ChangePassword.tsx @@ -6,7 +6,8 @@ import { Alert, Keyboard, KeyboardAvoidingView, - Platform + Platform, + useColorScheme } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Feather } from '@expo/vector-icons'; @@ -29,7 +30,6 @@ import { import { Controller, useForm } from 'react-hook-form'; import { router } from 'expo-router'; import { AlertTriangle, EyeIcon, EyeOffIcon } from 'lucide-react-native'; -import { useColorScheme, Switch } from 'react-native'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import GradientButton from '@/components/GradientButton'; @@ -37,6 +37,8 @@ import * as SecureStore from 'expo-secure-store'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import axios from 'axios'; import { Toast, ToastTitle, useToast } from '@gluestack-ui/themed'; +import { updateSecurity } from '@/utils/user'; +import { useTheme } from '@/components/ThemeContext'; const COLORS = { white: '#FFFFFF', @@ -59,67 +61,29 @@ const SIZES = { type SignUpSchemaType = z.infer; const ChangePassword = () => { - let colorScheme = useColorScheme(); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; const toast = useToast(); const onSubmit = async (_data: SignUpSchemaType) => { - //integration here - let userEmail = await SecureStore.getItemAsync('Email'); - console.log(JSON.stringify({ - email: userEmail, + if (_data.password === _data.confirmpassword) { + const settings = { currentPassword: _data.currentpassword, newPassword: _data.password, newPasswordConfirm: _data.confirmpassword - })); - if (_data.password === _data.confirmpassword) { - let userEmail = await SecureStore.getItemAsync('Email'); - let authToken = await SecureStore.getItemAsync('Token'); - - try { - const response = await axios.post('https://dev.occupi.tech/api/update-security-settings', { - email: userEmail, - currentPassword: _data.currentpassword, - newPassword: _data.password, - newPasswordConfirm: _data.confirmpassword - }, { - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - withCredentials: true - }); - const data = response.data; - // console.log(`Response Data: ${JSON.stringify(data.data)}`); - console.log(data); - if (response.status === 200) { - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - Password successfully changed - - ); - }, - }); - router.replace('/settings'); - } else { - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - console.log(data); - } - } catch (error) { - console.error('Error:', error); - } + }; + const response = await updateSecurity('password', settings) + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); + }, + }); } else if (_data.currentpassword === _data.password) { Alert.alert('Error', 'New password cannot be the same as the current password'); @@ -189,7 +153,7 @@ const ChangePassword = () => { return ( - + { as={Feather} name="chevron-left" size="xl" - color={colorScheme === 'dark' ? 'white' : 'black'} + color={currentTheme === 'dark' ? 'white' : 'black'} onPress={() => router.back()} /> - + Change Password @@ -218,7 +182,7 @@ const ChangePassword = () => { - Current Password + Current Password { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { onBlur={onBlur} onSubmitEditing={handleKeyPress} returnKeyType="done" - color={colorScheme === 'dark' ? 'white' : 'black'} + color={currentTheme === 'dark' ? 'white' : 'black'} type={showPassword ? 'text' : 'password'} /> @@ -265,7 +229,7 @@ const ChangePassword = () => { - New Password + New Password { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { onChangeText={onChange} onBlur={onBlur} onSubmitEditing={handleKeyPress} - color={colorScheme === 'dark' ? 'white' : 'black'} + color={currentTheme === 'dark' ? 'white' : 'black'} returnKeyType="done" type={showPassword ? 'text' : 'password'} /> @@ -312,7 +276,7 @@ const ChangePassword = () => { - Confirm Password + Confirm Password { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { onChangeText={onChange} onBlur={onBlur} onSubmitEditing={handleKeyPress} - color={colorScheme === 'dark' ? 'white' : 'black'} + color={currentTheme === 'dark' ? 'white' : 'black'} returnKeyType="done" type={showConfirmPassword ? 'text' : 'password'} /> diff --git a/frontend/occupi-mobile4/screens/Settings/FAQPage.tsx b/frontend/occupi-mobile4/screens/Settings/FAQPage.tsx index dbe72326..78c91732 100644 --- a/frontend/occupi-mobile4/screens/Settings/FAQPage.tsx +++ b/frontend/occupi-mobile4/screens/Settings/FAQPage.tsx @@ -1,10 +1,13 @@ import React from 'react'; import { ScrollView, useColorScheme } from 'react-native'; import { View, Text, Accordion, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent } from '@gluestack-ui/themed'; +import { useTheme } from '@/components/ThemeContext'; const FAQPage = () => { - const colorScheme = useColorScheme(); - const isDarkMode = colorScheme === 'dark'; + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const isDarkMode = currentTheme === 'dark'; const faqData = [ { diff --git a/frontend/occupi-mobile4/screens/Settings/Notifications.tsx b/frontend/occupi-mobile4/screens/Settings/Notifications.tsx index cd82dde9..4ba2ad4b 100644 --- a/frontend/occupi-mobile4/screens/Settings/Notifications.tsx +++ b/frontend/occupi-mobile4/screens/Settings/Notifications.tsx @@ -17,6 +17,8 @@ import GradientButton from '@/components/GradientButton'; import * as SecureStore from 'expo-secure-store'; import axios from 'axios'; import { Toast, ToastTitle, useToast } from '@gluestack-ui/themed'; +import { updateNotifications } from '@/utils/user'; +import { useTheme } from '@/components/ThemeContext'; const COLORS = { @@ -38,7 +40,9 @@ const SIZES = { }; const Notifications = () => { - let colorScheme = useColorScheme(); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; const toast = useToast(); //retrieve user settings ad assign variables accordingly const [oldInviteVal, setOldInviteVal] = useState(false); @@ -88,60 +92,21 @@ const Notifications = () => { }; const onSave = async () => { - let userEmail = await SecureStore.getItemAsync('Email'); - let authToken = await SecureStore.getItemAsync('Token'); - - try { - const response = await axios.get('https://dev.occupi.tech/api/update-notification-settings', { - params: { - email: userEmail, - invites: newInviteVal ? "on" : "off", - bookingReminder: newNotifyVal ? "on" : "off" - }, - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - withCredentials: true - }); - const data = response.data; - // console.log(`Response Data: ${JSON.stringify(data.data)}`); - console.log(data); - if (response.status === 200) { - const newSettings = { - invites: newInviteVal ? "on" : "off", - bookingReminder: newNotifyVal ? "on" : "off", - } - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - console.log(newSettings); - SecureStore.setItemAsync('Notifications', JSON.stringify(newSettings)); - router.replace('/settings'); - } else { - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - console.log(data); - } - } catch (error) { - console.error('Error:', error); - } + const settings = { + invites: newInviteVal ? "on" : "off", + bookingReminder: newNotifyVal ? "on" : "off" + }; + const response = await updateNotifications(settings) + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); + }, + }); }; const handleBack = () => { @@ -166,29 +131,29 @@ const Notifications = () => { } return ( - + - + Notifications - - Notify when someone invites me + + Notify when someone invites me { value={newInviteVal} /> - - Notify 15 minutes before booking time + + Notify 15 minutes before booking time { - const [selectedGenderIndex, setSelectedGenderIndex] = useState(1); + const [selectedGenderIndex, setSelectedGenderIndex] = useState('Male'); const [name, setName] = useState(''); const [email, setEmail] = useState(''); - const [employeeId, setEmployeeId] = useState(''); + const [employeeId, setEmployeeId] = useState('OCCUPI20242417'); const [phoneNumber, setPhoneNumber] = useState(''); const [pronouns, setPronouns] = useState(''); const [date, setDate] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isDatePickerVisible, setDatePickerVisibility] = useState(false); - let colorScheme = useColorScheme(); - const apiUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; - const getUserDetailsUrl= process.env.EXPO_PUBLIC_GET_USER_DETAILS; - const updateDetailsUrl = process.env.EXPO_PUBLIC_UPDATE_USER_DETAILS; - console.log(apiUrl, getUserDetailsUrl, updateDetailsUrl); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; + const toast = useToast(); + // console.log(apiUrl, getUserDetailsUrl, updateDetailsUrl); useEffect(() => { const getUserDetails = async () => { let result = await SecureStore.getItemAsync('UserData'); - console.log("UserData:",result); - // setUserDetails(JSON.parse(result).data); - let jsonresult = JSON.parse(result); - // console.log(jsonresult.data.details.name); - setName(String(jsonresult?.data?.details?.name)); - setEmail(String(jsonresult?.data?.email)); - setEmployeeId(String(jsonresult?.data?.occupiId)); - setPhoneNumber(String(jsonresult?.data?.details?.contactNo)); - setPronouns(String(jsonresult?.data?.details?.pronouns)); - const dateString = jsonresult?.data?.details?.dob; - const date = new Date(dateString); + console.log(result); + const email = await SecureStore.getItemAsync('Email'); + + let user = JSON.parse(result); + setName(String(user?.name)); + setEmail(String(email)); + setEmployeeId(String(user?.employeeid)); + setPhoneNumber(String(user?.number)); + setPronouns(String(user?.pronouns)); + setSelectedGenderIndex(String(user?.gender)) + const dateString = user?.dob; + console.log('dateee',dateString); + + // Manually parse the date string + const [datePart] = dateString.split('T'); + const [year, month, day] = datePart.split('-').map(Number); + + // Create a new Date object + const date = new Date(year, month, day); + // console.log(date.getDate()); // Get the day, month, and year - const day = date.getDate(); - const month = date.getMonth() + 1; // Months are zero-based - const year = date.getFullYear(); + const formattedDay = date.getDate(); + const formattedMonth = date.getMonth(); // Months are zero-based + const formattedYear = date.getFullYear(); // Format the date as MM/DD/YYYY - const formatted = `${month}/${day}/${year}`; + const formatted = `${formattedYear}-${formattedMonth}-${formattedDay}`; + // console.log(formatted); // Set the formatted date in the state - setDate(formatted) - - // console.log(JSON.parse(result).data.details.name); + setDate(formatted); }; getUserDetails(); }, []); @@ -101,81 +114,31 @@ const Profile = () => { setDatePickerVisibility(false); }; - const handleConfirm = (selectedDate) => { - setDate(selectedDate); + const handleConfirm = (selectedDate: string) => { + console.log('selected',extractDateFromTimestamp(selectedDate)); + setDate(extractDateFromTimestamp(selectedDate)); hideDatePicker(); }; - const onSave = async () => { - const body = { - "email": email, - "details": { - "contactNo": phoneNumber, - "gender": "Male", - "name": name, - "pronouns": pronouns - } - }; - // console.log(JSON.stringify(body)); - setIsLoading(true); - try { - let authToken = await SecureStore.getItemAsync('Token'); - const response = await fetch(`${apiUrl}${updateDetailsUrl}`, { - method: 'PUT', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - body: JSON.stringify(body), - credentials: "include" - }); - const data = await response.json(); - console.log(data); - if (response.ok) { - console.log(response); - setIsLoading(false); - alert('Details updated successfully'); - } else { - console.log(data); - setIsLoading(false); - } - } catch (error) { - setIsLoading(false); - console.error('Error:', error); - // setResponse('An error occurred'); - } - try { - let authToken = await SecureStore.getItemAsync('Token'); - const response = await fetch(`${apiUrl}${getUserDetailsUrl}?email=${email}`, { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - if (response.ok) { - saveUserData(JSON.stringify(data)); - console.log(data); - } else { - console.log(data); - } - } catch (error) { - console.error('Error:', error); - } + const onSave = async () => { + const response = await updateDetails(name,date,selectedGenderIndex,phoneNumber,pronouns) + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); + }, + }); }; - async function saveUserData(value) { - await SecureStore.setItemAsync('UserData', value); - } return ( @@ -183,37 +146,37 @@ const Profile = () => { as={Feather} name={"chevron-left"} size="xl" - color={colorScheme === 'dark' ? 'white' : 'black'} + color={currentTheme === 'dark' ? 'white' : 'black'} onPress={() => router.replace('/settings')} /> - + My account - Full name + Full name - Date of birth + Date of birth - + {date} - + { onCancel={hideDatePicker} /> - Gender - {/* setSelectedGenderIndex(index)}> + Gender + setSelectedGenderIndex(index)}> - Male + Male - Female + Female - Other + Other - */} - - Email Address + + Email Address - Occupi ID + Occupi ID - Cell No + Cell No - Pronouns (optional) + Pronouns (optional) { - let colorScheme = useColorScheme(); + const colorScheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorScheme : theme; const toast = useToast(); //retrieve user settings ad assign variables accordingly @@ -60,8 +56,9 @@ const Security = () => { useEffect(() => { const getSecurityDetails = async () => { let settings = await SecureStore.getItemAsync('Security'); + // console.log(settings); const settingsObject = JSON.parse(settings); - console.log(settingsObject); + // console.log('current settings',settingsObject); if (settingsObject.mfa === "on") { setOldMfa(true); @@ -119,59 +116,22 @@ const Security = () => { const onSave = async () => { //integration here - let userEmail = await SecureStore.getItemAsync('Email'); - let authToken = await SecureStore.getItemAsync('Token'); - - try { - const response = await axios.post('https://dev.occupi.tech/api/update-security-settings', { - email: userEmail, - mfa: newMfa ? "on" : "off", - forceLogout: newForceLogout ? "on" : "off" - }, { - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - withCredentials: true - }); - const data = response.data; - // console.log(`Response Data: ${JSON.stringify(data.data)}`); - // console.log(data); - if (response.status === 200) { - const newSettings = { - mfa: newMfa ? "on" : "off", - forceLogout: newForceLogout ? "on" : "off", - } - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - // console.log(newSettings); - SecureStore.setItemAsync('Security', JSON.stringify(newSettings)); - router.replace('/settings'); - } else { - console.log(data); - toast.show({ - placement: 'top', - render: ({ id }) => { - return ( - - {data.message} - - ); - }, - }); - } - } catch (error) { - console.error('Error:', error); - } + const settings = { + mfa: newMfa ? "on" : "off", + forceLogout: newForceLogout ? "on" : "off" + }; + const response = await updateSecurity('settings', settings) + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); + }, + }); + // console.log(newSettings); }; const handleBack = () => { @@ -195,282 +155,52 @@ const Security = () => { } } return ( - - - -<<<<<<< HEAD - - - - - - Security - - - - - - - - Use faceid/touch id to enter app - - - - Use 2fa to login - - - - Force logout on app close - - - - Use 2fa to login - - - - Force logout on app close - - - handleBiometricAuth()}> - - Change Password - - Change password - - - Current Password - - { - try { - await signUpSchema.parseAsync({ - password: value, - }); - return true; - } catch (error) { - return error.message; - } - }, - }} - render={({ field: { onChange, onBlur, value } }) => ( - - - - - - - )} - /> - - - - {errors?.password?.message} - - - - - - - New Password - - { - try { - await signUpSchema.parseAsync({ - password: value, - }); - return true; - } catch (error) { - return error.message; - } - }, - }} - render={({ field: { onChange, onBlur, value } }) => ( - - - - - - - )} - /> - - - - {errors?.password?.message} - - - - - - - Confirm Password - - { - try { - await signUpSchema.parseAsync({ - password: value, - }); - - return true; - } catch (error: any) { - return error.message; - } - }, - }} - render={({ field: { onChange, onBlur, value } }) => ( - - - - - - - - )} - /> - - - - {errors?.confirmpassword?.message} - - - - - -======= + - + Security ->>>>>>> develop - - Use 2fa to login + + Use 2fa to login - - Force logout on app close + + Force logout on app close handleBiometricAuth()}> - - Change Password + + Change Password diff --git a/frontend/occupi-mobile4/screens/Settings/Settings.tsx b/frontend/occupi-mobile4/screens/Settings/Settings.tsx index ced7ac53..4b5c565e 100644 --- a/frontend/occupi-mobile4/screens/Settings/Settings.tsx +++ b/frontend/occupi-mobile4/screens/Settings/Settings.tsx @@ -8,122 +8,111 @@ import { Icon, Divider, Pressable, + Toast, + ToastTitle, Text } from '@gluestack-ui/themed'; -import { useNavigation } from '@react-navigation/native'; import { Feather, MaterialIcons } from '@expo/vector-icons'; import { router } from 'expo-router'; import Navbar from '../../components/NavBar'; import { useColorScheme } from 'react-native'; import { widthPercentageToDP as wp } from 'react-native-responsive-screen'; import * as SecureStore from 'expo-secure-store'; +import { useToast } from '@gluestack-ui/themed'; +import { UserLogout } from '@/utils/auth'; +import { useTheme } from '@/components/ThemeContext'; const Settings = () => { const [name, setName] = useState(''); const [position, setPosition] = useState(''); - const navigation = useNavigation(); - let colorScheme = useColorScheme(); + const toast = useToast(); + const colorscheme = useColorScheme(); + const { theme } = useTheme(); + const currentTheme = theme === "system" ? colorscheme : theme; useEffect(() => { const getUserDetails = async () => { let result = await SecureStore.getItemAsync('UserData'); let jsonresult = JSON.parse(result); - setName(String(jsonresult.data.details.name)); - setPosition(String(jsonresult.data.position)); + setName(String(jsonresult.name)); + setPosition(String(jsonresult.position)); }; getUserDetails(); }, []); const handleLogout = async () => { - let authToken = await SecureStore.getItemAsync('Token'); - try { - const response = await fetch('https://dev.occupi.tech/auth/logout', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `${authToken}` - }, - credentials: "include" - }); - const data = await response.json(); - if (response.ok) { - console.log(data); - alert("logged out successfully"); - router.replace('/login'); - } else { - console.log(data); - alert("unable to logout"); + const response = await UserLogout(); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {response} + + ); } - } catch (error) { - console.error('Error:', error); - } + }); } - // console.log("details"+name); - - const handleNavigate = (screen) => { - navigation.navigate(screen); - }; const data = [ { title: 'My account', description: 'Make changes to your account', iconName: 'user', onPress: () => router.replace('/profile')}, { title: 'Notifications', description: 'Manage your notifications', iconName: 'bell', onPress: () => router.push('set-notifications')}, { title: 'Security', description: 'Enhance your security', iconName: 'shield', onPress: () => router.push('/set-security') }, { title: 'Appearance', description: 'Customize your viewing experience', iconName: 'image', onPress: () => router.push('/set-appearance') }, - { title: 'FAQ', description: '', iconName: 'info', onPress: () => router.push('faqpage') }, + { title: 'FAQ', description: "View the community's FAQ", iconName: 'info', onPress: () => router.push('faqpage') }, { title: 'Log out', description: 'Log out from your account', iconName: 'log-out', onPress: () => handleLogout() }, ]; const renderListItem = ({ item }) => ( - - + + - {item.title} - {item.description} + {item.title} + {item.description} - {item.accessoryRight ? item.accessoryRight() : } + {item.accessoryRight ? item.accessoryRight() : } ); return ( <> - +
- +
- {name} - {/* handleNavigate('EditProfileScreen')} /> */} + {name} + {/* handleNavigate('EditProfileScreen')} /> */} - {position} + {/* {position} */}
- - + + {data.map((item, index) => ( {renderListItem({ item })} - + ))}
- Version 0.1.0 + Version 0.1.0
@@ -191,7 +180,7 @@ const styles = StyleSheet.create({ color: '#fff', }, footerContainer: { - padding: 16, + // padding: 16, alignItems: 'center', }, versionText: { diff --git a/frontend/occupi-mobile4/services/__tests__/apiservices-test.tsx b/frontend/occupi-mobile4/services/__tests__/apiservices-test.tsx new file mode 100644 index 00000000..d760d73a --- /dev/null +++ b/frontend/occupi-mobile4/services/__tests__/apiservices-test.tsx @@ -0,0 +1,265 @@ +import axios from 'axios'; +import * as SecureStore from "expo-secure-store"; +import { + getUserDetails, + getNotificationSettings, + getUserBookings, + getSecuritySettings, + updateSecuritySettings, + updateNotificationSettings, +} from "../apiservices"; +import { NotificationSettingsReq } from '@/models/requests'; + +jest.mock('axios'); +jest.mock("expo-secure-store"); + +const mockedAxios = axios as jest.Mocked; + +describe("User API Functions", () => { + const mockEmail = "test@example.com"; + const mockAuthToken = "mockAuthToken"; + const mockSuccessResponse = { success: true, data: {} }; + const mockErrorResponse = { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + }; + + beforeEach(() => { + jest.resetAllMocks(); + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(mockAuthToken); + mockedAxios.isAxiosError.mockImplementation((payload: any) => true); + }); + + describe("getUserDetails", () => { + it("should return success response when API call is successful", async () => { + mockedAxios.get.mockResolvedValue({ data: mockSuccessResponse }); + + const result = await getUserDetails(mockEmail, mockAuthToken); + + expect(mockedAxios.get).toHaveBeenCalledWith( + "https://dev.occupi.tech/api/user-details", + expect.objectContaining({ + params: { email: mockEmail }, + headers: expect.objectContaining({ Authorization: mockAuthToken }), + }) + ); + expect(result).toEqual(mockSuccessResponse); + }); + + it("should return error response when API call fails", async () => { + mockedAxios.get.mockRejectedValue({ + response: { data: mockErrorResponse }, + }); + + const result = await getUserDetails(mockEmail, mockAuthToken); + + expect(result).toEqual(mockErrorResponse); + }); + }); + + describe("getNotificationSettings", () => { + it("should return success response when API call is successful", async () => { + (axios.get as jest.Mock).mockResolvedValue({ data: mockSuccessResponse }); + + const result = await getNotificationSettings(mockEmail); + + expect(axios.get).toHaveBeenCalledWith( + "https://dev.occupi.tech/api/get-notification-settings", + expect.objectContaining({ + params: { email: mockEmail }, + headers: expect.objectContaining({ Authorization: mockAuthToken }), + }) + ); + expect(result).toEqual(mockSuccessResponse); + }); + + it("should return error response when API call fails", async () => { + (axios.get as jest.Mock).mockRejectedValue({ + response: { data: mockErrorResponse }, + }); + + const result = await getNotificationSettings(mockEmail); + + expect(result).toEqual(mockErrorResponse); + }); + }); + + describe("getUserBookings", () => { + it("should return success response when API call is successful", async () => { + const mockAuthToken = "mockAuthToken"; + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(mockAuthToken); + (axios.get as jest.Mock).mockResolvedValue({ + data: { + data: { bookings: [] }, + status: 'success', + message: 'Bookings retrieved successfully' + } + }); + + const result = await getUserBookings(mockEmail); + + expect(axios.get).toHaveBeenCalledWith( + `https://dev.occupi.tech/api/view-bookings?filter={"email":"${mockEmail}"}`, + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: mockAuthToken }), + }) + ); + expect(result).toEqual({ + data: { bookings: [] }, + status: 'success', + message: 'Bookings retrieved successfully' + }); + }); + + it("should return error response when API call fails", async () => { + const mockAuthToken = "mockAuthToken"; + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(mockAuthToken); + (axios.get as jest.Mock).mockRejectedValue({ + response: { + data: { + data: null, + status: 'error', + message: 'Failed to retrieve bookings', + error: { + code: 'API_ERROR', + details: 'API call failed', + message: 'Failed to retrieve bookings' + } + } + }, + }); + + const result = await getUserBookings(mockEmail); + + expect(result).toEqual({ + data: null, + status: 'error', + message: 'Failed to retrieve bookings', + error: { + code: 'API_ERROR', + details: 'API call failed', + message: 'Failed to retrieve bookings' + } + }); + }); + + it("should handle case when auth token is not found", async () => { + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(null); + + const result = await getUserBookings(mockEmail); + + expect(result).toEqual({ + data: null, + status: 'error', + message: 'Authentication failed', + error: { + code: 'AUTH_ERROR', + details: 'No authentication token found', + message: 'Authentication failed' + } + }); + }); + }); + + describe("getSecuritySettings", () => { + it("should return success response when API call is successful", async () => { + (axios.get as jest.Mock).mockResolvedValue({ data: mockSuccessResponse }); + + const result = await getSecuritySettings(mockEmail); + + expect(axios.get).toHaveBeenCalledWith( + "https://dev.occupi.tech/api/get-security-settings", + expect.objectContaining({ + params: { email: mockEmail }, + headers: expect.objectContaining({ Authorization: mockAuthToken }), + }) + ); + + expect(result).toEqual(mockSuccessResponse); + }); + + it("should return error response when API call fails", async () => { + (axios.get as jest.Mock).mockRejectedValue({ + response: { data: mockErrorResponse }, + }); + + const result = await getSecuritySettings(mockEmail); + + expect(result).toEqual(mockErrorResponse); + }); + }); + + describe("updateSecuritySettings", () => { + it("should return success response when API call is successful", async () => { + (axios.post as jest.Mock).mockResolvedValue({ + data: mockSuccessResponse, + }); + const mockReq = { email: mockEmail, newSetting: "value" }; + + const result = await updateSecuritySettings(mockReq); + + expect(axios.post).toHaveBeenCalledWith( + "https://dev.occupi.tech/api/update-security-settings", + mockReq, + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: mockAuthToken }), + }) + ); + expect(result).toEqual(mockSuccessResponse); + }); + + it("should return error response when API call fails", async () => { + (axios.post as jest.Mock).mockRejectedValue({ + response: { data: mockErrorResponse }, + }); + const mockReq = { email: mockEmail, newSetting: "value" }; + + const result = await updateSecuritySettings(mockReq); + + expect(result).toEqual(mockErrorResponse); + }); + }); + + describe("updateNotificationSettings", () => { + it("should return success response when API call is successful", async () => { + (axios.get as jest.Mock).mockResolvedValue({ data: mockSuccessResponse }); + const mockReq: NotificationSettingsReq = { + email: mockEmail, + invites: "on", + bookingReminder: "on", + }; + + const result = await updateNotificationSettings(mockReq); + + expect(axios.get).toHaveBeenCalledWith( + "https://dev.occupi.tech/api/update-notification-settings", + expect.objectContaining({ + params: { req: mockReq }, + headers: expect.objectContaining({ Authorization: mockAuthToken }), + }) + ); + expect(result).toEqual(mockSuccessResponse); + }); + + it("should return error response when API call fails", async () => { + (axios.get as jest.Mock).mockRejectedValue({ + response: { data: mockErrorResponse }, + }); + const mockReq: NotificationSettingsReq = { + email: mockEmail, + invites: "on", + bookingReminder: "on", + }; + + const result = await updateNotificationSettings(mockReq); + + expect(result).toEqual(mockErrorResponse); + }); + }); +}); diff --git a/frontend/occupi-mobile4/services/__tests__/authservices-test.tsx b/frontend/occupi-mobile4/services/__tests__/authservices-test.tsx new file mode 100644 index 00000000..31c8dc3a --- /dev/null +++ b/frontend/occupi-mobile4/services/__tests__/authservices-test.tsx @@ -0,0 +1,137 @@ +import axios from 'axios'; +import * as SecureStore from 'expo-secure-store'; +import { login, logout } from '../../services/authservices'; +import { LoginReq } from '@/models/requests'; +import { LoginSuccess, Unsuccessful, Success } from '@/models/response'; + +jest.mock('axios'); +jest.mock('expo-secure-store'); + +const mockedAxios = axios as jest.Mocked; +const mockedSecureStore = SecureStore as jest.Mocked; + +describe('authservice', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockedSecureStore.getItemAsync.mockResolvedValue('mock-token'); + }); + + describe('login', () => { + const loginReq: LoginReq = { + email: 'test@example.com', + password: 'password123', + }; + + it('should return LoginSuccess on successful login', async () => { + const mockResponse: LoginSuccess = { + data: { token: 'mock-token' }, + message: 'Login successful', + status: 200, + }; + + mockedAxios.post.mockResolvedValueOnce({ data: mockResponse }); + + const result = await login(loginReq); + + expect(mockedAxios.post).toHaveBeenCalledWith( + 'https://dev.occupi.tech/auth/login-mobile', + loginReq, + expect.any(Object) + ); + expect(result).toEqual(mockResponse); + }); + + it('should throw error on failed login', async () => { + const mockError: Unsuccessful = { + data: null, + status: 'error', + message: 'Invalid credentials', + error: { + code: 'AUTH_ERROR', + details: 'Invalid email or password', + message: 'Authentication failed', + } + }; + + mockedAxios.post.mockRejectedValueOnce({ + isAxiosError: true, + response: { data: mockError } + }); + + await expect(login(loginReq)).rejects.toEqual( + expect.objectContaining({ + isAxiosError: true, + response: { data: mockError } + }) + ); + }); + + it('should throw error on network failure', async () => { + const networkError = new Error('Network error'); + mockedAxios.post.mockRejectedValueOnce(networkError); + + await expect(login(loginReq)).rejects.toThrow('Network error'); + }); + }); + + describe('logout', () => { + beforeEach(() => { + mockedSecureStore.getItemAsync.mockResolvedValue('mock-token'); + }); + + it('should return Success on successful logout', async () => { + const mockResponse: Success = { + status: 200, + message: 'Logout successful', + data: null, + }; + + mockedAxios.post.mockResolvedValueOnce({ data: mockResponse }); + + const result = await logout(); + + expect(mockedAxios.post).toHaveBeenCalledWith( + 'https://dev.occupi.tech/auth/logout', + {}, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'mock-token', + }), + }) + ); + expect(result).toEqual(mockResponse); + }); + + it('should throw error on failed logout', async () => { + const mockError: Unsuccessful = { + data: null, + status: 'error', + message: 'Logout failed', + error: { + code: 'LOGOUT_ERROR', + details: 'Unable to logout', + message: 'Logout operation failed', + } + }; + + mockedAxios.post.mockRejectedValueOnce({ + isAxiosError: true, + response: { data: mockError } + }); + + await expect(logout()).rejects.toEqual( + expect.objectContaining({ + isAxiosError: true, + response: { data: mockError } + }) + ); + }); + + it('should throw error on network failure', async () => { + const networkError = new Error('Network error'); + mockedAxios.post.mockRejectedValueOnce(networkError); + + await expect(logout()).rejects.toThrow('Network error'); + }); + }); +}); diff --git a/frontend/occupi-mobile4/services/__tests__/securestore-test.tsx b/frontend/occupi-mobile4/services/__tests__/securestore-test.tsx new file mode 100644 index 00000000..be6a6a6e --- /dev/null +++ b/frontend/occupi-mobile4/services/__tests__/securestore-test.tsx @@ -0,0 +1,83 @@ +import * as SecureStore from 'expo-secure-store'; +import { + storeUserData, + storeToken, + getUserData, + getToken, + deleteUserData, + deleteAllData, + } from '../securestore'; + + jest.mock('expo-secure-store'); + +const mockedSecureStore = SecureStore as jest.Mocked; + + describe('SecureStore', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('storeUserData', () => { + it('should store user data', async () => { + const userData = JSON.stringify({ id: 1, name: 'John Doe' }); + await storeUserData(userData); + expect(mockedSecureStore.setItemAsync).toHaveBeenCalledWith('UserData', userData); + }); + }); + + describe('storeToken', () => { + it('should store token', async () => { + const token = 'abc123'; + await storeToken(token); + expect(mockedSecureStore.setItemAsync).toHaveBeenCalledWith('Token', token); + }); + }); + + describe('getUserData', () => { + it('should return parsed user data when it exists', async () => { + const userData = { id: 1, name: 'John Doe' }; + mockedSecureStore.getItemAsync.mockResolvedValue(JSON.stringify(userData)); + const result = await getUserData(); + expect(result).toEqual(userData); + }); + + it('should return null when token does not exist', async () => { + mockedSecureStore.getItemAsync.mockResolvedValue(null); + const result = await getUserData(); // Corrected this line + expect(result).toBeNull(); + }); + }); + + describe('getToken', () => { + it('should return token when it exists', async () => { + const token = 'abc123'; + mockedSecureStore.getItemAsync.mockResolvedValue(token); + const result = await getToken(); + expect(result).toBe(token); + }); + + it('should return undefined when token does not exist', async () => { + mockedSecureStore.getItemAsync.mockResolvedValue(null); + const result = await getToken(); + expect(result).toBeUndefined(); + }); + }); + + describe('deleteUserData', () => { + it('should delete user data', async () => { + await deleteUserData(); + expect(mockedSecureStore.deleteItemAsync).toHaveBeenCalledWith('UserData'); + }); + }); + + describe('deleteAllData', () => { + it('should delete all data', async () => { + await deleteAllData(); + expect(mockedSecureStore.deleteItemAsync).toHaveBeenCalledWith('UserData'); + expect(mockedSecureStore.deleteItemAsync).toHaveBeenCalledWith('Token'); + expect(mockedSecureStore.deleteItemAsync).toHaveBeenCalledWith('Email'); + }); + }); + + + }); \ No newline at end of file diff --git a/frontend/occupi-mobile4/services/apiservices.ts b/frontend/occupi-mobile4/services/apiservices.ts new file mode 100644 index 00000000..dea22a74 --- /dev/null +++ b/frontend/occupi-mobile4/services/apiservices.ts @@ -0,0 +1,376 @@ +import { Success, Unsuccessful } from "@/models/response"; +import { SecuritySettingsReq, NotificationSettingsReq, CheckInReq, CancelBookingReq, BookRoomReq, NotificationsReq, UpdateDetailsReq } from "@/models/requests"; +// import axios from 'axios'; +import * as SecureStore from 'expo-secure-store'; +import axios, { AxiosError } from 'axios'; +import { storeUserData } from "./securestore"; + +export const getUserDetails = async (email: string, authToken: string): Promise => { + try { + const response = await axios.get("https://dev.occupi.tech/api/user-details", { + params: { email }, + headers: { Authorization: authToken }, + }); + return response.data as Success; + } catch (error) { + console.error(`Error in getUserDetails:`, error); + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + if (axiosError.response?.data) { + return axiosError.response.data; + } + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + }; + } +}; + +export async function getNotificationSettings(email: string): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + // console.log(authToken); + try { + const response = await axios.get(`https://dev.occupi.tech/api/get-notification-settings`, { + params: { + email: email + }, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `${authToken}` + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export const getUserBookings = async (email: string): Promise => { + try { + const authToken = await SecureStore.getItemAsync("Token"); + if (!authToken) { + return { + data: null, + status: 'error', + message: 'Authentication failed', + error: { + code: 'AUTH_ERROR', + details: 'No authentication token found', + message: 'Authentication failed' + } + }; + } + + const response = await axios.get( + `https://dev.occupi.tech/api/view-bookings?filter={"email":"${email}"}`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: authToken, + }, + withCredentials: true, + } + ); + // console.log(response.data); + return response.data; + } catch (error) { + console.error("Error in getUserBookings:", error); + if (axios.isAxiosError(error) && error.response) { + return error.response.data; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +}; + +export async function getNotifications(req: NotificationsReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + try { + const response = await axios.get("https://dev.occupi.tech/api/get-notifications", { + params: req, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function checkin(req: CheckInReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + console.log(req); + try { + const response = await axios.post("https://dev.occupi.tech/api/check-in", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + console.log(error.response.data) + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function updateUserDetails(req: UpdateDetailsReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + console.log(req); + try { + const response = await axios.post("https://dev.occupi.tech/api/update-user", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + storeUserData(JSON.stringify(req)); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + console.log(error.response.data) + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function bookRoom(req: BookRoomReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + console.log(req); + try { + const response = await axios.post("https://dev.occupi.tech/api/book-room", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + console.log(error.response.data) + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function cancelBooking(req: CancelBookingReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + console.log(req); + try { + const response = await axios.post("https://dev.occupi.tech/api/cancel-booking", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + console.log(error.response.data) + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function getSecuritySettings(email: string): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + // console.log(authToken); + try { + const response = await axios.get(`https://dev.occupi.tech/api/get-security-settings`, { + params: { + email: email + }, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `${authToken}` + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function updateSecuritySettings(req: SecuritySettingsReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + try { + const response = await axios.post("https://dev.occupi.tech/api/update-security-settings", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} + +export async function updateNotificationSettings(req: NotificationSettingsReq): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + try { + const response = await axios.get("https://dev.occupi.tech/api/update-notification-settings", { + params: { + req + }, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as Success; + } catch (error) { + console.error(`Error in ${Function}:`, error); + if (axios.isAxiosError(error) && error.response?.data) { + return error.response.data as Unsuccessful; + } + return { + data: null, + status: 'error', + message: 'An unexpected error occurred', + error: { + code: 'UNKNOWN_ERROR', + details: 'An unexpected error occurred', + message: 'An unexpected error occurred' + } + } as Unsuccessful; + } +} \ No newline at end of file diff --git a/frontend/occupi-mobile4/services/authservices.ts b/frontend/occupi-mobile4/services/authservices.ts new file mode 100644 index 00000000..06ed7ed6 --- /dev/null +++ b/frontend/occupi-mobile4/services/authservices.ts @@ -0,0 +1,123 @@ +import { LoginReq, RegisterReq, VerifyOTPReq } from "@/models/requests"; +import { LoginSuccess, Unsuccessful, Success } from "@/models/response"; +import axios from 'axios'; +import dotenv from 'dotenv'; +import * as SecureStore from 'expo-secure-store'; + +// dotenv.config(); +// const devUrl = process.env.EXPO_PUBLIC_DEVELOP_API_URL; +// console.log(devUrl); + +export async function login(req: LoginReq): Promise { + try { + const response = await axios.post("https://dev.occupi.tech/auth/login-mobile", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as LoginSuccess; + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + // console.log(error.response.data); + return error.response.data as Unsuccessful; + } else { + throw error; + } + } +} + +export async function register(req: RegisterReq): Promise { + console.log(req); + try { + const response = await axios.post("https://dev.occupi.tech/auth/register", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + withCredentials: true + }); + console.log(response.data); + return response.data as Success; + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + console.log(error.response.data); + return error.response.data as Unsuccessful; + } else { + throw error; + } + } +} + +export async function verifyOtpRegister(req: VerifyOTPReq): Promise { + try { + const response = await axios.post("https://dev.occupi.tech/auth/verify-otp", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as LoginSuccess; + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + // console.log(error.response.data); + return error.response.data as Unsuccessful; + } else { + throw error; + } + } +} + +export async function verifyOtplogin(req: VerifyOTPReq): Promise { + try { + const response = await axios.post("https://dev.occupi.tech/auth/verify-otp-mobile-login", req, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as LoginSuccess; + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + // console.log(error.response.data); + return error.response.data as Unsuccessful; + } else { + throw error; + } + } +} + +export async function logout(): Promise { + let authToken = await SecureStore.getItemAsync('Token'); + // console.log('token',authToken); + try { + const response = await axios.post("https://dev.occupi.tech/auth/logout", {},{ + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `${authToken}` + }, + withCredentials: true + }); + // console.log(response.data); + return response.data as Success; + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + // console.log(error.response.data); + return error.response.data as Unsuccessful; + } else { + throw error; + } + } +} + +// login({ +// email: "boygenius31115@gmail.com", +// password: "Qwert@123" +// }) \ No newline at end of file diff --git a/frontend/occupi-mobile4/services/securestore.ts b/frontend/occupi-mobile4/services/securestore.ts new file mode 100644 index 00000000..2f34575c --- /dev/null +++ b/frontend/occupi-mobile4/services/securestore.ts @@ -0,0 +1,82 @@ +import * as SecureStore from 'expo-secure-store'; +import { User } from '@/models/data'; + +export async function storeUserData(value: string) { + await SecureStore.setItemAsync('UserData', value); +} + +export async function storeToken(value: string) { + await SecureStore.setItemAsync('Token', value); +} + +export async function storeUserEmail(value: string) { + await SecureStore.setItemAsync('Email', value); +} + +export async function setState(value: string) { + await SecureStore.setItemAsync('AppState', value); +} + +export async function storeNotificationSettings(value: string) { + await SecureStore.setItemAsync('Notifications', value); +} + +export async function storeTheme(value: string) { + await SecureStore.setItemAsync('Theme', value); +} + +export async function storeAccentColour(value: string) { + await SecureStore.setItemAsync('accentColour', value); +} + +export async function storeSecuritySettings(value: string) { + await SecureStore.setItemAsync('Security', value); +} + +export async function getUserData() { + let result: string | null = await SecureStore.getItemAsync('UserData'); + return result ? JSON.parse(result) : null; +} + +export async function getToken() { + let result = await SecureStore.getItemAsync('Token'); + const tokenVal = result; + // console.log('token', result); + return tokenVal || undefined; +} + +export async function getUserEmail() { + let result = await SecureStore.getItemAsync('Email'); + return result; +} + +export async function getCurrentRoom() { + let result = await SecureStore.getItemAsync('CurrentRoom'); + return result; +} + +export async function deleteUserData() { + await SecureStore.deleteItemAsync('UserData'); +} + +export async function deleteToken() { + await SecureStore.deleteItemAsync('Token'); +} + +export async function deleteUserEmail() { + await SecureStore.deleteItemAsync('Email'); +} + +export async function deleteNotificationSettings() { + await SecureStore.deleteItemAsync('Notifications'); +} + +export async function deleteSecuritySettings() { + await SecureStore.deleteItemAsync('Security'); +} + +export async function deleteAllData() { + await SecureStore.deleteItemAsync('UserData'); + await SecureStore.deleteItemAsync('Token'); + await SecureStore.deleteItemAsync('Email'); +} \ No newline at end of file diff --git a/frontend/occupi-mobile4/tsconfig.json b/frontend/occupi-mobile4/tsconfig.json index 909e9010..00af20ff 100644 --- a/frontend/occupi-mobile4/tsconfig.json +++ b/frontend/occupi-mobile4/tsconfig.json @@ -1,11 +1,13 @@ { "extends": "expo/tsconfig.base", "compilerOptions": { + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "baseUrl": ".", "strict": true, + "types": ["jest", "node"], "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] } }, "include": [ @@ -14,4 +16,4 @@ ".expo/types/**/*.ts", "expo-env.d.ts" ] -} +} \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/__tests__/auth-test.tsx b/frontend/occupi-mobile4/utils/__tests__/auth-test.tsx new file mode 100644 index 00000000..0e3721c5 --- /dev/null +++ b/frontend/occupi-mobile4/utils/__tests__/auth-test.tsx @@ -0,0 +1,134 @@ +import { UserLogin, UserLogout } from '../auth'; +import { login, logout } from "../../services/authservices"; +import { fetchNotificationSettings, fetchSecuritySettings, fetchUserDetails } from "../user"; +import { router } from 'expo-router'; +import { storeUserEmail, storeToken, setState, deleteToken, deleteUserData, deleteUserEmail, deleteNotificationSettings, deleteSecuritySettings } from "../../services/securestore"; + +// Mock dependencies +jest.mock('../../services/authservices'); +jest.mock('../user'); +jest.mock('expo-router', () => ({ + router: { + replace: jest.fn(), + }, +})); +jest.mock('../../services/securestore'); + +describe('auth.ts', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('UserLogin', () => { + it('should login successfully and set up user data', async () => { + const mockEmail = 'test@example.com'; + const mockPassword = 'password123'; + const mockToken = 'mock-token'; + + (login as jest.Mock).mockResolvedValue({ + status: 200, + data: { token: mockToken }, + message: 'Login successful' + }); + + const result = await UserLogin(mockEmail, mockPassword); + + expect(storeUserEmail).toHaveBeenCalledWith(mockEmail); + expect(login).toHaveBeenCalledWith({ email: mockEmail, password: mockPassword }); + expect(setState).toHaveBeenCalledWith('logged_in'); + expect(storeToken).toHaveBeenCalledWith(mockToken); + expect(fetchUserDetails).toHaveBeenCalledWith(mockEmail, mockToken); + expect(fetchNotificationSettings).toHaveBeenCalledWith(mockEmail); + expect(fetchSecuritySettings).toHaveBeenCalledWith(mockEmail); + expect(router.replace).toHaveBeenCalledWith('/home'); + expect(result).toBe('Login successful'); + }); + + it('should handle login failure', async () => { + const mockEmail = 'test@example.com'; + const mockPassword = 'wrong-password'; + + (login as jest.Mock).mockResolvedValue({ + status: 401, + message: 'Invalid credentials' + }); + + const result = await UserLogin(mockEmail, mockPassword); + + expect(storeUserEmail).toHaveBeenCalledWith(mockEmail); + expect(login).toHaveBeenCalledWith({ email: mockEmail, password: mockPassword }); + expect(setState).not.toHaveBeenCalled(); + expect(storeToken).not.toHaveBeenCalled(); + expect(fetchUserDetails).not.toHaveBeenCalled(); + expect(fetchNotificationSettings).not.toHaveBeenCalled(); + expect(fetchSecuritySettings).not.toHaveBeenCalled(); + expect(router.replace).not.toHaveBeenCalled(); + expect(result).toBe('Invalid credentials'); + }); + + it('should handle errors during login', async () => { + const mockEmail = 'test@example.com'; + const mockPassword = 'password123'; + + (login as jest.Mock).mockRejectedValue(new Error('Network error')); + + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await UserLogin(mockEmail, mockPassword); + + expect(consoleSpy).toHaveBeenCalledWith('Error:', expect.any(Error)); + consoleSpy.mockRestore(); + }); + }); + + describe('UserLogout', () => { + it('should logout successfully and clear user data', async () => { + (logout as jest.Mock).mockResolvedValue({ + status: 200, + message: 'Logout successful' + }); + + const result = await UserLogout(); + + expect(logout).toHaveBeenCalled(); + expect(setState).toHaveBeenCalledWith('logged_out'); + expect(deleteNotificationSettings).toHaveBeenCalled(); + expect(deleteSecuritySettings).toHaveBeenCalled(); + expect(deleteUserData).toHaveBeenCalled(); + expect(deleteToken).toHaveBeenCalled(); + expect(deleteUserEmail).toHaveBeenCalled(); + expect(router.replace).toHaveBeenCalledWith('/login'); + expect(result).toBe('Logout successful'); + }); + + it('should handle logout failure', async () => { + (logout as jest.Mock).mockResolvedValue({ + status: 400, + message: 'Logout failed' + }); + + const result = await UserLogout(); + + expect(logout).toHaveBeenCalled(); + expect(setState).not.toHaveBeenCalled(); + expect(deleteNotificationSettings).not.toHaveBeenCalled(); + expect(deleteSecuritySettings).not.toHaveBeenCalled(); + expect(deleteUserData).not.toHaveBeenCalled(); + expect(deleteToken).not.toHaveBeenCalled(); + expect(deleteUserEmail).not.toHaveBeenCalled(); + expect(router.replace).not.toHaveBeenCalled(); + expect(result).toBe('Logout failed'); + }); + + it('should handle errors during logout', async () => { + (logout as jest.Mock).mockRejectedValue(new Error('Network error')); + + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await UserLogout(); + + expect(consoleSpy).toHaveBeenCalledWith('Error:', expect.any(Error)); + consoleSpy.mockRestore(); + }); + }); +}); \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/__tests__/bookings-test.tsx b/frontend/occupi-mobile4/utils/__tests__/bookings-test.tsx new file mode 100644 index 00000000..b59f3e88 --- /dev/null +++ b/frontend/occupi-mobile4/utils/__tests__/bookings-test.tsx @@ -0,0 +1,125 @@ +import { fetchUserBookings, userCheckin, userCancelBooking } from '../bookings'; +import { getUserBookings, checkin, cancelBooking } from '../../services/apiservices'; +import * as SecureStore from 'expo-secure-store'; +import { router } from 'expo-router'; + +// Mock dependencies +jest.mock('../../services/apiservices'); +jest.mock('expo-secure-store'); +jest.mock('expo-router', () => ({ + router: { + replace: jest.fn(), + }, +})); + +beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('../bookings.ts', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('fetchUserBookings', () => { + it('should fetch user bookings successfully', async () => { + const mockEmail = 'test@example.com'; + const mockBookings = [{ id: 1, title: 'Booking 1' }, { id: 2, title: 'Booking 2' }]; + + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(mockEmail); + (getUserBookings as jest.Mock).mockResolvedValue({ status: 200, data: mockBookings }); + + const result = await fetchUserBookings(); + + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('Email'); + expect(getUserBookings).toHaveBeenCalledWith(mockEmail); + expect(result).toEqual(mockBookings); + }); + + it('should handle errors when fetching user bookings', async () => { + const mockError = new Error('API Error'); + + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue('test@example.com'); + (getUserBookings as jest.Mock).mockRejectedValue(mockError); + + await expect(fetchUserBookings()).rejects.toThrow('API Error'); + }); + }); + + describe('userCheckin', () => { + it('should perform user check-in successfully', async () => { + const mockRoom = { occupiId: 'room123' }; + const mockEmail = 'test@example.com'; + + (SecureStore.getItemAsync as jest.Mock) + .mockResolvedValueOnce(JSON.stringify(mockRoom)) + .mockResolvedValueOnce(mockEmail); + (checkin as jest.Mock).mockResolvedValue({ status: 200, message: 'Check-in successful' }); + + const result = await userCheckin(); + + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('CurrentRoom'); + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('Email'); + expect(checkin).toHaveBeenCalledWith({ email: mockEmail, bookingId: 'room123' }); + expect(result).toBe('Check-in successful'); + }); + + it('should handle errors during user check-in', async () => { + const mockError = new Error('Check-in Error'); + + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue('{}'); + (checkin as jest.Mock).mockRejectedValue(mockError); + + await expect(userCheckin()).rejects.toThrow('Check-in Error'); + }); + }); + + describe('userCancelBooking', () => { + it('should cancel user booking successfully', async () => { + const mockRoom = { + occupiId: 'booking123', + emails: ['user1@example.com', 'user2@example.com'], + roomId: 'room123', + creator: 'user1@example.com', + date: '2023-07-30', + start: '10:00', + end: '11:00', + floorNo: 1, + roomName: 'Meeting Room A' + }; + + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(JSON.stringify(mockRoom)); + (cancelBooking as jest.Mock).mockResolvedValue({ status: 200, message: 'Booking cancelled successfully' }); + + const result = await userCancelBooking(); + + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('CurrentRoom'); + expect(cancelBooking).toHaveBeenCalledWith(expect.objectContaining({ + bookingId: 'booking123', + emails: ['user1@example.com', 'user2@example.com'], + roomId: 'room123', + creator: 'user1@example.com', + date: '2023-07-30', + start: '10:00', + end: '11:00', + floorNo: 1, + roomName: 'Meeting Room A' + })); + expect(router.replace).toHaveBeenCalledWith('/home'); + expect(result).toBe('Booking cancelled successfully'); + }); + + it('should handle errors during booking cancellation', async () => { + const mockError = new Error('Cancellation Error'); + + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue('{}'); + (cancelBooking as jest.Mock).mockRejectedValue(mockError); + + await expect(userCancelBooking()).rejects.toThrow('Cancellation Error'); + }); + }); +}); \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/__tests__/notifications-test.tsx b/frontend/occupi-mobile4/utils/__tests__/notifications-test.tsx new file mode 100644 index 00000000..af726b8f --- /dev/null +++ b/frontend/occupi-mobile4/utils/__tests__/notifications-test.tsx @@ -0,0 +1,90 @@ +import * as Notifications from 'expo-notifications'; +import { sendPushNotification } from '../notifications'; // Adjust the import path as needed + +jest.mock('expo-notifications'); +jest.mock('node-fetch'); + +describe('Notification Functions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('setNotificationHandler sets the correct handler', () => { + // Manually call setNotificationHandler to ensure it's executed + Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: false, + shouldSetBadge: false, + }), + }); + + expect(Notifications.setNotificationHandler).toHaveBeenCalledWith(expect.objectContaining({ + handleNotification: expect.any(Function), + })); + }); + + test('handleNotification returns correct configuration', async () => { + // Manually call setNotificationHandler to ensure it's executed + Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: false, + shouldSetBadge: false, + }), + }); + + const handler = (Notifications.setNotificationHandler as jest.Mock).mock.calls[0][0]; + const result = await handler.handleNotification(); + expect(result).toEqual({ + shouldShowAlert: true, + shouldPlaySound: false, + shouldSetBadge: false, + }); + }); + + test('sendPushNotification sends notifications to all tokens', async () => { + const mockFetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ data: 'success' }), + }); + global.fetch = mockFetch as any; + + const expoPushTokens = ['token1', 'token2', 'token3']; + const title = 'Test Title'; + const body = 'Test Body'; + + await sendPushNotification(expoPushTokens, title, body); + + expect(mockFetch).toHaveBeenCalledTimes(3); + + expoPushTokens.forEach((token, index) => { + expect(mockFetch).toHaveBeenNthCalledWith(index + 1, 'https://exp.host/--/api/v2/push/send', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Accept-encoding': 'gzip, deflate', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + to: token, + sound: 'default', + title: title, + body: body, + data: { someData: 'goes here' }, + }), + }); + }); + }); + + test('sendPushNotification handles errors', async () => { + const mockFetch = jest.fn().mockRejectedValue(new Error('Network error')); + global.fetch = mockFetch as any; + + const expoPushTokens = ['token1']; + const title = 'Test Title'; + const body = 'Test Body'; + + await expect(sendPushNotification(expoPushTokens, title, body)).rejects.toThrow('Network error'); + }); +}); \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/__tests__/user-test.tsx b/frontend/occupi-mobile4/utils/__tests__/user-test.tsx new file mode 100644 index 00000000..8c4d81ab --- /dev/null +++ b/frontend/occupi-mobile4/utils/__tests__/user-test.tsx @@ -0,0 +1,127 @@ +import * as SecureStore from 'expo-secure-store'; +import { + storeUserData, + storeToken, + storeUserEmail, + setState, + storeNotificationSettings, + storeSecuritySettings, + getUserData, + getToken, + getUserEmail, + getCurrentRoom, + deleteUserData, + deleteToken, + deleteUserEmail, + deleteNotificationSettings, + deleteSecuritySettings, + deleteAllData +} from '../../services/securestore'; + +jest.mock('expo-secure-store'); + +describe('Secure Store Functions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('storeUserData stores user data', async () => { + const userData = JSON.stringify({ name: 'John Doe', email: 'john@example.com' }); + await storeUserData(userData); + expect(SecureStore.setItemAsync).toHaveBeenCalledWith('UserData', userData); + }); + + test('storeToken stores token', async () => { + const token = 'abc123'; + await storeToken(token); + expect(SecureStore.setItemAsync).toHaveBeenCalledWith('Token', token); + }); + + test('storeUserEmail stores email', async () => { + const email = 'john@example.com'; + await storeUserEmail(email); + expect(SecureStore.setItemAsync).toHaveBeenCalledWith('Email', email); + }); + + test('setState stores app state', async () => { + const state = 'active'; + await setState(state); + expect(SecureStore.setItemAsync).toHaveBeenCalledWith('AppState', state); + }); + + test('storeNotificationSettings stores notification settings', async () => { + const settings = JSON.stringify({ pushEnabled: true }); + await storeNotificationSettings(settings); + expect(SecureStore.setItemAsync).toHaveBeenCalledWith('Notifications', settings); + }); + + test('storeSecuritySettings stores security settings', async () => { + const settings = JSON.stringify({ twoFactor: true }); + await storeSecuritySettings(settings); + expect(SecureStore.setItemAsync).toHaveBeenCalledWith('Security', settings); + }); + + test('getUserData retrieves user data', async () => { + const userData = { name: 'John Doe', email: 'john@example.com' }; + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(JSON.stringify(userData)); + const result = await getUserData(); + expect(result).toEqual(userData); + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('UserData'); + }); + + test('getToken retrieves token', async () => { + const token = 'abc123'; + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(token); + const result = await getToken(); + expect(result).toBe(token); + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('Token'); + }); + + test('getUserEmail retrieves email', async () => { + const email = 'john@example.com'; + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(email); + const result = await getUserEmail(); + expect(result).toBe(email); + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('Email'); + }); + + test('getCurrentRoom retrieves current room', async () => { + const room = 'living-room'; + (SecureStore.getItemAsync as jest.Mock).mockResolvedValue(room); + const result = await getCurrentRoom(); + expect(result).toBe(room); + expect(SecureStore.getItemAsync).toHaveBeenCalledWith('CurrentRoom'); + }); + + test('deleteUserData deletes user data', async () => { + await deleteUserData(); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('UserData'); + }); + + test('deleteToken deletes token', async () => { + await deleteToken(); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('Token'); + }); + + test('deleteUserEmail deletes email', async () => { + await deleteUserEmail(); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('Email'); + }); + + test('deleteNotificationSettings deletes notification settings', async () => { + await deleteNotificationSettings(); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('Notifications'); + }); + + test('deleteSecuritySettings deletes security settings', async () => { + await deleteSecuritySettings(); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('Security'); + }); + + test('deleteAllData deletes all data', async () => { + await deleteAllData(); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('UserData'); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('Token'); + expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('Email'); + }); +}); \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/auth.ts b/frontend/occupi-mobile4/utils/auth.ts new file mode 100644 index 00000000..603a04ae --- /dev/null +++ b/frontend/occupi-mobile4/utils/auth.ts @@ -0,0 +1,144 @@ +//this folder contains functions that will call the service functions which make api requests for authentication +//the purpose of this file is to refine and process the data and return these to the View + +import { login, logout, register, verifyOtplogin, verifyOtpRegister } from "../services/authservices"; +import { fetchNotificationSettings, fetchSecuritySettings, fetchUserDetails } from "./user"; +import { router } from 'expo-router'; +import { storeUserEmail, storeToken, setState, deleteToken, deleteUserData, deleteUserEmail, deleteNotificationSettings, deleteSecuritySettings } from "../services/securestore"; +import { retrievePushToken } from "./notifications"; + + +export async function UserLogin(email: string, password: string) { + storeUserEmail(email); + try { + const response = await login({ + email: email, + password: password + }); + if (response.status === 200) { + console.log('responseee',response); + if (response.data !== null) { + setState('logged_in'); + storeToken(response.data.token); + console.log('here'); + fetchUserDetails(email, response.data.token); + fetchNotificationSettings(email); + fetchSecuritySettings(email); + router.replace('/home'); + } + else { + setState('verify_otp_login'); + router.replace('verify-otp') + } + + return response.message; + } + else { + console.log('woahhh', response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function userRegister(email: string, password: string, employeeId: string) { + let expoPushToken = await retrievePushToken(); + storeUserEmail(email); + try { + const response = await register({ + email: email, + password: password, + // employee_id: employeeId, + expoPushToken: expoPushToken + }); + if (response.status === 200) { + console.log('responseee',response); + setState('verify_otp_register'); + router.replace('/verify-otp'); + return response.message; + } + else { + console.log('woahhh', response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function verifyUserOtpRegister(email: string, otp: string) { + try { + const response = await verifyOtpRegister({ + email: email, + otp: otp + }); + if (response.status === 200) { + console.log('responseee',response); + router.replace('/set-details'); + router.replace('/login'); + return response.message; + } + else { + console.log('woahhh', response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function VerifyUserOtpLogin(email : string, otp : string) { + try { + const response = await verifyOtplogin({ + email: email, + otp: otp + }); + if (response.status === 200) { + console.log('responseee',response); + if (response.data !== null) { + setState('logged_in'); + storeToken(response.data.token); + console.log('here'); + fetchUserDetails(email, response.data.token); + fetchNotificationSettings(email); + fetchSecuritySettings(email); + router.replace('/home'); + } + + return response.message; + } + else { + console.log('woahhh', response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function UserLogout() { + // console.log('hhhh'); + try { + const response = await logout(); + if (response.status === 200) { + // console.log('responseee',response); + setState('logged_out'); + deleteNotificationSettings(); + deleteSecuritySettings(); + deleteUserData(); + deleteToken(); + deleteUserEmail(); + router.replace('/login'); + return response.message; + } + else { + console.log('woahhh', response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +// UserLogin("kamogelomoeketse@gmail.com", "Qwerty@123"); //test \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/bookings.ts b/frontend/occupi-mobile4/utils/bookings.ts new file mode 100644 index 00000000..91cfede8 --- /dev/null +++ b/frontend/occupi-mobile4/utils/bookings.ts @@ -0,0 +1,101 @@ +import { Booking, Room } from "@/models/data"; +import { bookRoom, cancelBooking, checkin, getUserBookings } from "../services/apiservices"; +import * as SecureStore from 'expo-secure-store'; +import { router } from 'expo-router'; +import { BookRoomReq, CancelBookingReq } from "@/models/requests"; + +export async function fetchUserBookings(): Promise { + let email = await SecureStore.getItemAsync('Email'); + try { + const response = await getUserBookings(email); + if (response.status === 200) { + // console.log('response', response.data); + return response.data; + // console.log(settings); + } + else { + console.log(response) + } + return response.data as Booking[]; + } catch (error) { + console.error('Error:', error); + throw error; // Add a throw statement to handle the error case + } +} + +export async function userBookRoom(attendees : string[], startTime : string, endTime : string) { + let roomstring = await SecureStore.getItemAsync("BookingInfo"); + let email = await SecureStore.getItemAsync("Email"); + const room : Booking = JSON.parse(roomstring as string); + const body : BookRoomReq = { + roomName: room.roomName, + creator: email, + date: room.date+"T00:00:00.000+00:00", + start: room.date+"T"+startTime+":00.000+00:00", + end: room.date+"T"+endTime+":00.000+00:00", + floorNo: room.floorNo, + emails: attendees, + roomId: room.roomId + + } + console.log(body); + try { + const response = await bookRoom(body); + if (response.status === 200) { + return response.message; + } + return response.message; + } catch (error) { + console.error('Error:', error); + throw error; + } +} + +export async function userCheckin() { + let roomstring = await SecureStore.getItemAsync("CurrentRoom"); + const room = JSON.parse(roomstring as string); + const bookingId = room?.occupiId; + let email = await SecureStore.getItemAsync('Email'); + const body = { + email: email as string, + bookingId: bookingId + } + try { + const response = await checkin(body); + if (response.status === 200) { + return response.message; + } + return response.message; + } catch (error) { + console.error('Error:', error); + throw error; + } +} + +export async function userCancelBooking() { + let roomstring = await SecureStore.getItemAsync("CurrentRoom"); + const room : Booking = JSON.parse(roomstring as string); + let email = await SecureStore.getItemAsync('Email'); + const body : CancelBookingReq = { + bookingId: room?.occupiId, + emails: room?.emails, + roomId: room?.roomId, + creator: room.creator, + date: room?.date, + start: room?.start, + end: room?.end, + floorNo: room?.floorNo, + roomName: room?.roomName + } + try { + const response = await cancelBooking(body); + if (response.status === 200) { + router.replace('/home'); + return response.message; + } + return response.message; + } catch (error) { + console.error('Error:', error); + throw error; + } +} diff --git a/frontend/occupi-mobile4/utils/dashboard.ts b/frontend/occupi-mobile4/utils/dashboard.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/occupi-mobile4/utils/notifications.ts b/frontend/occupi-mobile4/utils/notifications.ts new file mode 100644 index 00000000..f0d1ff7d --- /dev/null +++ b/frontend/occupi-mobile4/utils/notifications.ts @@ -0,0 +1,117 @@ +import * as Device from 'expo-device'; +import { Platform } from 'react-native'; +import * as Notifications from 'expo-notifications'; +import Constants from 'expo-constants'; +import * as SecureStore from 'expo-secure-store'; +import { NotificationsReq } from '@/models/requests'; +import { getNotifications } from '@/services/apiservices'; + +Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: false, + shouldSetBadge: false, + }), +}); + +export async function retrievePushToken(): Promise { + const token = await registerForPushNotificationsAsync(); + // console.log(token); + return token as string; +} + +// retrievePushToken(); +// console.log('yurp'); + +async function registerForPushNotificationsAsync() { + let token; + + if (Platform.OS === 'android') { + await Notifications.setNotificationChannelAsync('default', { + name: 'default', + importance: Notifications.AndroidImportance.MAX, + vibrationPattern: [0, 250, 250, 250], + lightColor: '#FF231F7C', + }); + } + + if (Device.isDevice) { + const { status: existingStatus } = await Notifications.getPermissionsAsync(); + let finalStatus = existingStatus; + if (existingStatus !== 'granted') { + const { status } = await Notifications.requestPermissionsAsync(); + finalStatus = status; + } + if (finalStatus !== 'granted') { + alert('Failed to get push token for push notification!'); + return; + } + // Learn more about projectId: + // https://docs.expo.dev/push-notifications/push-notifications-setup/#configure-projectid + // EAS projectId is used here. + try { + const projectId = + Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId; + if (!projectId) { + throw new Error('Project ID not found'); + } + token = ( + await Notifications.getExpoPushTokenAsync({ + projectId, + }) + ).data; + // console.log(token); + } catch (e) { + token = `${e}`; + } + } else { + alert('Must use physical device for Push Notifications'); + } + + return token; +} + + export async function sendPushNotification(expoPushTokens: string[], title: string, body: string) { + const messages = expoPushTokens.map(token => ({ + to: token, + sound: 'default', + title: title, + body: body, + data: { someData: 'goes here' }, + })); + + for (const message of messages) { + await fetch('https://exp.host/--/api/v2/push/send', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Accept-encoding': 'gzip, deflate', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(message), + }); + } +} + +export async function getUserNotifications() { + let email = await SecureStore.getItemAsync('Email'); + + try { + const request : NotificationsReq = { + filter: { + emails: [email] + } + }; + const response = await getNotifications(request); + if (response.status === 200) { + // console.log('notifications', response.data); + return response.data + } + else { + console.log(response) + return response.data; + } + } catch (error) { + console.error('Error:', error); + } +} \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/occupancy.ts b/frontend/occupi-mobile4/utils/occupancy.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/occupi-mobile4/utils/settings.ts b/frontend/occupi-mobile4/utils/settings.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/occupi-mobile4/utils/user.ts b/frontend/occupi-mobile4/utils/user.ts new file mode 100644 index 00000000..8b3e70b2 --- /dev/null +++ b/frontend/occupi-mobile4/utils/user.ts @@ -0,0 +1,182 @@ +import { UpdateDetailsReq } from "@/models/requests"; +import { getUserDetails, getNotificationSettings, getSecuritySettings, updateSecuritySettings, updateNotificationSettings, updateUserDetails } from "../services/apiservices"; +import { storeUserData, storeNotificationSettings, getUserData, storeSecuritySettings, setState } from "../services/securestore"; +import { router } from 'expo-router'; +import * as SecureStore from 'expo-secure-store'; + + +export async function fetchUserDetails(email: string, token: string) { + try { + const response = await getUserDetails(email, token); + if (response.status === 200) { + storeUserData(JSON.stringify(response.data)); + } + else { + console.log(response) + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function fetchNotificationSettings(email: string) { + try { + const response = await getNotificationSettings(email); + if (response.status === 200) { + const settings = { + invites: response.data.invites, + bookingReminder: response.data.bookingReminder + }; + // console.log('settings response', response.data); + // console.log(settings); + storeNotificationSettings(JSON.stringify(settings)); + } + else { + console.log(response) + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function fetchSecuritySettings(email: string) { + try { + const response = await getSecuritySettings(email); + if (response.status === 200) { + const settings = { + mfa: response.data.mfa, + forcelogout: response.data.forceLogout + }; + // console.log('settings response', response.data); + // console.log(settings); + storeSecuritySettings(JSON.stringify(settings)); + } + else { + console.log(response) + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function updateSecurity(type: string, values: any) { + let userData = await SecureStore.getItemAsync('UserData'); + let user = JSON.parse(userData || ""); + let email = user.email; + if (type === "settings") { + try { + const request = { + email: email, + mfa: values.mfa, + forceLogout: values.forceLogout + } + const response = await updateSecuritySettings(request); + if (response.status === 200) { + const settings = { + mfa: values.mfa, + forceLogout: values.forceLogout + }; + console.log('settings response', response); + console.log(settings); + storeSecuritySettings(JSON.stringify(settings)); + router.replace('/settings') + return "Settings updated successfully" + } + else { + console.log(response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } + } else { + try { + const request = { + email: email, + currentPassword: values.currentPassword, + newPassword: values.newPassword, + newPasswordConfirm: values.newPasswordConfirm + } + const response = await updateSecuritySettings(request); + if (response.status === 200) { + router.replace('/set-security') + return "Successfully changed password" + } + else { + console.log(response); + return response.message; + } + } catch (error) { + console.error('Error:', error); + } + } +} + +export async function updateDetails(name: string, dob: string, gender: string, cellno: string, pronouns: string) { + const email = await SecureStore.getItemAsync('Email'); + const state = await SecureStore.getItemAsync('AppState'); + try { + const request : UpdateDetailsReq = { + session_email: email, + name: name, + dob: dob + "T00:00:00.000Z", + gender: gender, + number: cellno, + pronouns: pronouns, + } + const response = await updateUserDetails(request); + if (response.status === 200) { + console.log(response); + if (state === "verify_otp_register") { + setState("logged_out"); + router.replace('login'); + } + router.replace('/settings') + return "Details updated successfully" + } + else { + console.log(response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function updateNotifications(values: any) { + let userData = await SecureStore.getItemAsync('UserData'); + let user = JSON.parse(userData || ""); + let email = user.email; + try { + const request = { + email: email, + invites: values.mfa, + bookingReminder: values.forceLogout + } + const response = await updateNotificationSettings(request); + if (response.status === 200) { + const settings = { + invites: values.invites, + bookingReminder: values.bookingReminder + }; + console.log('settings response', response); + console.log(settings); + storeNotificationSettings(JSON.stringify(settings)); + router.replace('/settings') + return "Settings updated successfully" + } + else { + console.log(response) + return response.message; + } + } catch (error) { + console.error('Error:', error); + } +} + +export async function fetchUsername() { + let userData = await SecureStore.getItemAsync('UserData'); + let user = JSON.parse(userData || "{}"); + // console.log(user.name); + return user.name; +} \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/utils.ts b/frontend/occupi-mobile4/utils/utils.ts index 83af1778..d6ae0fbb 100644 --- a/frontend/occupi-mobile4/utils/utils.ts +++ b/frontend/occupi-mobile4/utils/utils.ts @@ -1,12 +1,33 @@ import * as SecureStore from 'expo-secure-store'; + + export const getAccentColour = async () => { let accentcolour = await SecureStore.getItemAsync('accentColour'); if (!accentcolour) { return "greenyellow"; } - else - { - return accentcolour; + else { + return accentcolour; + } +}; + +export const getTheme = async () => { + let theme = await SecureStore.getItemAsync('Theme'); + if (!theme) { + return "dark"; } - }; \ No newline at end of file + else { + return theme; + } +}; + +export const theme = getTheme(); + +export function extractDateFromTimestamp(timestamp: string): string { + const date = new Date(timestamp); + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); + const day = String(date.getUTCDate()+1).padStart(2, '0'); + return `${year}-${month}-${day}`; + } \ No newline at end of file diff --git a/frontend/occupi-mobile4/utils/viewbookings.ts b/frontend/occupi-mobile4/utils/viewbookings.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/occupi-web/.gitignore b/frontend/occupi-web/.gitignore index 034af8a1..3389aaee 100644 --- a/frontend/occupi-web/.gitignore +++ b/frontend/occupi-web/.gitignore @@ -31,6 +31,4 @@ dist-ssr .env.production.local *.crt -*.key - -coverage/ \ No newline at end of file +*.key \ No newline at end of file diff --git a/frontend/occupi-web/bun.lockb b/frontend/occupi-web/bun.lockb index b48bafbc..260cd133 100644 Binary files a/frontend/occupi-web/bun.lockb and b/frontend/occupi-web/bun.lockb differ diff --git a/frontend/occupi-web/jest.config.js b/frontend/occupi-web/jest.config.js new file mode 100644 index 00000000..2cc6083f --- /dev/null +++ b/frontend/occupi-web/jest.config.js @@ -0,0 +1,25 @@ +// jest.config.js +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['./jest.setup.ts'], + moduleNameMapper: { + '\\.(css|less|scss|sass)$': 'identity-obj-proxy', + '^@api/(.*)$': '/src/api/$1', + '^@assets/(.*)$': '/src/assets/$1', + '^@components/(.*)$': '/src/components/$1', + '^@config/(.*)$': '/src/config/$1', + '^@layouts/(.*)$': '/src/layouts/$1', + '^@lib/(.*)$': '/src/lib/$1', + '^@pages/(.*)$': '/src/pages/$1', + '^@services/(.*)$': '/src/services/$1', + '^@store/(.*)$': '/src/store/$1', + '^@utils/(.*)$': '/src/utils/$1' + }, + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + testPathIgnorePatterns: ['/node_modules/', '/dist/'], + }; + \ No newline at end of file diff --git a/frontend/occupi-web/jest.setup.ts b/frontend/occupi-web/jest.setup.ts new file mode 100644 index 00000000..26aa925c --- /dev/null +++ b/frontend/occupi-web/jest.setup.ts @@ -0,0 +1,2 @@ +// jest.setup.ts +import '@testing-library/jest-dom/extend-expect'; diff --git a/frontend/occupi-web/package-lock.json b/frontend/occupi-web/package-lock.json new file mode 100644 index 00000000..a3bc8723 --- /dev/null +++ b/frontend/occupi-web/package-lock.json @@ -0,0 +1,6606 @@ +{ + "name": "occupi-web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "occupi-web", + "version": "0.0.0", + "dependencies": { + "@headlessui/react": "^2.0.4", + "@heroicons/react": "^2.1.3", + "@radix-ui/react-checkbox": "^1.0.4", + "@testing-library/jest-dom": "^6.4.5", + "@testing-library/react": "^15.0.7", + "@types/jest": "^29.5.12", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "lucide-react": "^0.379.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwind-merge": "^2.3.0", + "tailwindcss": "^3.4.3" + }, + "devDependencies": { + "@types/node": "^20.12.12", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "jest": "^29.7.0", + "postcss": "^8.4.38", + "tailwindcss-animate": "^1.0.7", + "typescript": "^5.2.2", + "vite": "^5.2.11" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", + "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.4", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-simple-access": "^7.24.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz", + "integrity": "sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==", + "devOptional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.5", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.5", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk/node_modules/ansi-styles": { + "version": "3.2.1", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk/node_modules/ansi-styles/node_modules/color-convert": { + "version": "1.9.3", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/chalk/node_modules/ansi-styles/node_modules/color-convert/node_modules/color-name": { + "version": "1.1.3", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/chalk/node_modules/escape-string-regexp": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/chalk/node_modules/supports-color": { + "version": "5.5.0", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk/node_modules/supports-color/node_modules/has-flag": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.5", + "devOptional": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.6.tgz", + "integrity": "sha512-lWfvAIFNWMlCsU0DRUun2GpFwZdGTukLaHJqRh1JRb80NdAP5Sb1HDHB5X9P9OtgZHQl089UzQkpYlBq2VTPRw==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.6.tgz", + "integrity": "sha512-TzCtxGgVTEJWWwcYwQhCIQ6WaKlo80/B+Onsk4RRCcYqpYGFcG9etPW94VToGte5AAcxRrhjPUFvUS3Y2qKi4A==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz", + "integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.24.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "devOptional": true + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.2", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.16", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.0", + "@floating-ui/utils": "^0.2.0", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "license": "MIT" + }, + "node_modules/@headlessui/react": { + "version": "2.0.4", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.13", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@tanstack/react-virtual": "3.5.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18", + "react-dom": "^18" + } + }, + "node_modules/@heroicons/react": { + "version": "2.1.3", + "license": "MIT", + "peerDependencies": { + "react": ">= 16" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "devOptional": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "devOptional": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "devOptional": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "devOptional": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "devOptional": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "devOptional": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "devOptional": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "devOptional": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "devOptional": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "devOptional": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "devOptional": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "devOptional": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "devOptional": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "devOptional": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "devOptional": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "devOptional": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "devOptional": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "devOptional": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz", + "integrity": "sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", + "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", + "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@react-aria/focus": { + "version": "3.17.1", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.21.3", + "@react-aria/utils": "^3.24.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.21.3", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.4", + "@react-aria/utils": "^3.24.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.4", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.24.1", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.4", + "@react-stately/utils": "^3.10.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.1", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/shared": { + "version": "3.23.1", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.17.2", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.17.2", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "devOptional": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "devOptional": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.11", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.5.0", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.5.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz", + "integrity": "sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.5.tgz", + "integrity": "sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==", + "dependencies": { + "@adobe/css-tools": "^4.3.2", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/bun": "latest", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/bun": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "15.0.7", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-15.0.7.tgz", + "integrity": "sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^10.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "devOptional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.2", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/type-utils": "7.9.0", + "@typescript-eslint/utils": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.9.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/typescript-estree": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.9.0", + "@typescript-eslint/utils": "7.9.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.9.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/typescript-estree": "7.9.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.9.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.5", + "@babel/plugin-transform-react-jsx-self": "^7.23.3", + "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "devOptional": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "devOptional": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "devOptional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "devOptional": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "devOptional": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "devOptional": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "devOptional": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "devOptional": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "devOptional": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "devOptional": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001620", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "devOptional": true + }, + "node_modules/class-variance-authority": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", + "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "2.0.0" + }, + "funding": { + "url": "https://joebell.co.uk" + } + }, + "node_modules/class-variance-authority/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "devOptional": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "devOptional": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "devOptional": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "devOptional": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "devOptional": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.4", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "devOptional": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "license": "Apache-2.0" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.774", + "devOptional": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "devOptional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "devOptional": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.7", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "devOptional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "devOptional": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "devOptional": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "devOptional": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "devOptional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.1", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.4", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "devOptional": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "devOptional": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "devOptional": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "devOptional": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "devOptional": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "devOptional": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "devOptional": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "devOptional": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "devOptional": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "devOptional": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.1.2", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "devOptional": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "devOptional": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "devOptional": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "devOptional": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "devOptional": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "devOptional": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "devOptional": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "devOptional": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "devOptional": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "devOptional": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "devOptional": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "devOptional": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "devOptional": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "devOptional": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "devOptional": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "devOptional": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "devOptional": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "devOptional": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "devOptional": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "devOptional": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "devOptional": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "devOptional": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "devOptional": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "devOptional": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "devOptional": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.379.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.379.0.tgz", + "integrity": "sha512-KcdeVPqmhRldldAAgptb8FjIunM2x2Zy26ZBh1RsEUcdLIvsEmbcw7KpzFYUy5BbpGeWhPu9Z9J5YXfStiXwhg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "devOptional": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "devOptional": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "devOptional": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "devOptional": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "devOptional": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "devOptional": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "devOptional": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "devOptional": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "devOptional": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "devOptional": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "devOptional": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "devOptional": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.2", + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "devOptional": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "devOptional": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "devOptional": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "devOptional": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "devOptional": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "devOptional": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "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/react": { + "version": "18.3.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "devOptional": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.17.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "devOptional": true + }, + "node_modules/slash": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "devOptional": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "devOptional": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "devOptional": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz", + "integrity": "sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.3", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "devOptional": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "devOptional": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.6.2", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "devOptional": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vite": { + "version": "5.2.11", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "devOptional": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "devOptional": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "devOptional": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "devOptional": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.4.2", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "devOptional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "devOptional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/occupi-web/package.json b/frontend/occupi-web/package.json index 74ae1bf3..4f7d2ba6 100644 --- a/frontend/occupi-web/package.json +++ b/frontend/occupi-web/package.json @@ -31,7 +31,6 @@ "@types/babel__core": "^7.20.5", "@types/jest": "^29.5.12", "@types/prop-types": "^15.7.12", - "@types/react-beautiful-dnd": "^13.1.8", "axios": "^1.7.2", "body-parser": "^1.20.2", "bun-types": "^1.1.17", @@ -50,7 +49,6 @@ "lucide-react": "^0.381.0", "npm": "^10.8.1", "react": "^18.3.1", - "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.3.1", "react-icons": "^5.2.1", "react-pdf-charts": "^0.2.5", diff --git a/frontend/occupi-web/src/App.css b/frontend/occupi-web/src/App.css new file mode 100644 index 00000000..4014cd73 --- /dev/null +++ b/frontend/occupi-web/src/App.css @@ -0,0 +1,7 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + diff --git a/frontend/occupi-web/src/AuthService.ts b/frontend/occupi-web/src/AuthService.ts index ad2997c6..837b24bf 100644 --- a/frontend/occupi-web/src/AuthService.ts +++ b/frontend/occupi-web/src/AuthService.ts @@ -1,14 +1,89 @@ +// // src/services/AuthService.ts +// import axios, { AxiosResponse } from 'axios'; + +// const API_URL = import.meta.env.VITE_API_URL; +// console.log(API_URL); + +// if (!API_URL) { +// throw new Error('VITE_API_URL is not defined in the environment'); +// } + +// // ... rest of the AuthService code remains the same +// // ... rest of the AuthService code remains the same +// interface RegisterData { +// email: string; +// password: string; +// employee_id?: string; +// } + +// interface LoginData { +// email: string; +// password: string; +// } + +// interface VerifyOTPData { +// email: string; +// oTP: string; +// } + +// interface ApiResponse { +// status: number; +// message: string; +// data: any; +// } + +// const AuthService = { +// register: async (data: RegisterData): Promise => { +// try { +// const response: AxiosResponse = await axios.post(`${API_URL}/auth/register`, data); +// return response.data; +// } catch (error) { +// if (axios.isAxiosError(error) && error.response) { +// throw error.response.data; +// } +// throw new Error('An unexpected error occurred'); +// } +// }, + +// login: async (data: LoginData): Promise => { +// try { +// const response: AxiosResponse = await axios.post(`${API_URL}/auth/login`, data); +// return response.data; +// } catch (error) { +// if (axios.isAxiosError(error) && error.response) { +// throw error.response.data; +// } +// throw new Error('An unexpected error occurred'); +// } +// }, + +// verifyOTP: async (data: VerifyOTPData): Promise => { +// try { +// const response: AxiosResponse = await axios.post(`${API_URL}/auth/verify-otp`, data); +// return response.data; +// } catch (error) { +// if (axios.isAxiosError(error) && error.response) { +// throw error.response.data; +// } +// throw new Error('An unexpected error occurred'); +// } +// } +// }; + +// export default AuthService; + + import axios from 'axios'; -const API_URL = '/auth'; // This will be proxied to https://dev.occupi.tech -const API_USER_URL = '/api'; // Adjust this if needed +const API_URL = import.meta.env.VITE_API_URL; const AuthService = { login: async (email: string, password: string) => { try { - const response = await axios.post(`${API_URL}/login-admin`, { + const response = await axios.post(`${API_URL}/auth/login`, { email, password, + // employee_id }); return response.data; } catch (error) { diff --git a/frontend/occupi-web/src/assets/login.png b/frontend/occupi-web/src/assets/login.png new file mode 100644 index 00000000..261f040e Binary files /dev/null and b/frontend/occupi-web/src/assets/login.png differ diff --git a/frontend/occupi-web/src/assets/login.svg b/frontend/occupi-web/src/assets/login.svg new file mode 100644 index 00000000..0200e58a --- /dev/null +++ b/frontend/occupi-web/src/assets/login.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/occupi-web/src/assets/otp.png b/frontend/occupi-web/src/assets/otp.png new file mode 100644 index 00000000..9f52539e Binary files /dev/null and b/frontend/occupi-web/src/assets/otp.png differ diff --git a/frontend/occupi-web/src/assets/react.svg b/frontend/occupi-web/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/frontend/occupi-web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/occupi-web/src/components/InputBox/InputBox.test.tsx b/frontend/occupi-web/src/components/InputBox/InputBox.test.tsx index 14d1ba28..374be321 100644 --- a/frontend/occupi-web/src/components/InputBox/InputBox.test.tsx +++ b/frontend/occupi-web/src/components/InputBox/InputBox.test.tsx @@ -1,108 +1,129 @@ -/// - -import { render, screen, fireEvent, cleanup } from "@testing-library/react"; -import InputBox from "./InputBox"; -import { describe, test, expect, afterEach, mock } from "bun:test"; - -afterEach(() => { - cleanup(); -}); - -describe("InputBox", () => { - test("renders with correct label and placeholder", () => { - render( - {}} - /> - ); - const label = screen.getByText("Email"); - const input = screen.getByPlaceholderText("Enter your email"); - expect(label).toBeTruthy(); - expect(input).toBeTruthy(); - }); - - - test("validates password input correctly", () => { - const submitValueMock = mock(() => {}); - render( - - ); - - const input = screen.getByPlaceholderText("Enter your password"); - fireEvent.change(input, { target: { value: "weakpwd" } }); - const error = screen.getByText("Invalid Password"); - expect(error).toBeTruthy(); - expect(submitValueMock).toHaveBeenCalledWith("weakpwd", false); - - fireEvent.change(input, { target: { value: "StrongPwd123" } }); - expect(screen.queryByText("Invalid Password")).toBeNull(); - expect(submitValueMock).toHaveBeenCalledWith("StrongPwd123", true); - }); - - // New tests added below - - test("renders correctly with password type", () => { - const submitValueMock = mock(() => {}); - render( - - ); - - expect(screen.getByText("Password")).toBeTruthy(); - expect(screen.getByPlaceholderText("Enter your password")).toBeTruthy(); - }); - - test("applies error styles when input is invalid", () => { - const submitValueMock = mock(() => {}); - render( - - ); - - const input = screen.getByPlaceholderText("Enter your email"); - - fireEvent.change(input, { target: { value: "invalid-email" } }); - expect(input.className).toContain("border-[2px]"); - expect(input.className).toContain("border-red_salmon"); - - fireEvent.change(input, { target: { value: "valid@email.com" } }); - expect(input.className).not.toContain("border-[2px]"); - expect(input.className).not.toContain("border-red_salmon"); - }); - - test("clears error message when input becomes valid", () => { - const submitValueMock = mock(() => {}); - render( - - ); - - const input = screen.getByPlaceholderText("Enter your email"); - - fireEvent.change(input, { target: { value: "invalid-email" } }); - expect(screen.getByText("Invalid Email")).toBeTruthy(); - - fireEvent.change(input, { target: { value: "valid@email.com" } }); - expect(screen.queryByText("Invalid Email")).toBeNull(); - }); -}); \ No newline at end of file +// /// + +// import { render, screen, fireEvent, cleanup } from "@testing-library/react"; +// import InputBox from "./InputBox"; +// import { describe, test, expect, afterEach, mock } from "bun:test"; + +// afterEach(() => { +// cleanup(); +// }); + +// describe("InputBox", () => { +// test("renders with correct label and placeholder", () => { +// render( +// {}} +// /> +// ); +// const label = screen.getByText("Email"); +// const input = screen.getByPlaceholderText("Enter your email"); +// expect(label).toBeTruthy(); +// expect(input).toBeTruthy(); +// }); + +// // test("validates email input correctly", () => { +// // const submitValueMock = mock(() => {}); +// // render( +// // +// // ); + +// // const input = screen.getByPlaceholderText("Enter your email"); +// // fireEvent.change(input, { target: { value: "invalid-email" } }); +// // const error = screen.getByText("Invalid Email"); +// // expect(error).toBeTruthy(); +// // expect(submitValueMock).toHaveBeenCalledWith("invalid-email", false); + +// // fireEvent.change(input, { target: { value: "test@example.com" } }); +// // expect(screen.queryByText("Invalid Email")).toBeNull(); +// // expect(submitValueMock).toHaveBeenCalledWith("test@example.com", true); +// // }); + +// test("validates password input correctly", () => { +// const submitValueMock = mock(() => {}); +// render( +// +// ); + +// const input = screen.getByPlaceholderText("Enter your password"); +// fireEvent.change(input, { target: { value: "weakpwd" } }); +// const error = screen.getByText("Invalid Password"); +// expect(error).toBeTruthy(); +// expect(submitValueMock).toHaveBeenCalledWith("weakpwd", false); + +// fireEvent.change(input, { target: { value: "StrongPwd123" } }); +// expect(screen.queryByText("Invalid Password")).toBeNull(); +// expect(submitValueMock).toHaveBeenCalledWith("StrongPwd123", true); +// }); + +// // New tests added below + +// test("renders correctly with password type", () => { +// const submitValueMock = mock(() => {}); +// render( +// +// ); + +// expect(screen.getByText("Password")).toBeTruthy(); +// expect(screen.getByPlaceholderText("Enter your password")).toBeTruthy(); +// }); + +// test("applies error styles when input is invalid", () => { +// const submitValueMock = mock(() => {}); +// render( +// +// ); + +// const input = screen.getByPlaceholderText("Enter your email"); + +// fireEvent.change(input, { target: { value: "invalid-email" } }); +// expect(input.className).toContain("border-[2px]"); +// expect(input.className).toContain("border-red_salmon"); + +// fireEvent.change(input, { target: { value: "valid@email.com" } }); +// expect(input.className).not.toContain("border-[2px]"); +// expect(input.className).not.toContain("border-red_salmon"); +// }); + +// test("clears error message when input becomes valid", () => { +// const submitValueMock = mock(() => {}); +// render( +// +// ); + +// const input = screen.getByPlaceholderText("Enter your email"); + +// fireEvent.change(input, { target: { value: "invalid-email" } }); +// expect(screen.getByText("Invalid Email")).toBeTruthy(); + +// fireEvent.change(input, { target: { value: "valid@email.com" } }); +// expect(screen.queryByText("Invalid Email")).toBeNull(); +// }); +// }); \ No newline at end of file diff --git a/frontend/occupi-web/src/components/OtpComponent/OtpComponent.test.tsx b/frontend/occupi-web/src/components/OtpComponent/OtpComponent.test.tsx index 178f02e5..d948e0c0 100644 --- a/frontend/occupi-web/src/components/OtpComponent/OtpComponent.test.tsx +++ b/frontend/occupi-web/src/components/OtpComponent/OtpComponent.test.tsx @@ -2,7 +2,6 @@ // import { test, expect, mock, afterEach } from 'bun:test'; // import { render, fireEvent, cleanup } from '@testing-library/react'; // import OtpComponent from './OtpComponent'; // Adjust the import based on your file structure -// import { act } from 'react'; // afterEach(() => { @@ -19,43 +18,20 @@ // inputs.forEach(input => { // expect((input as HTMLInputElement).value).toEqual(''); // }); +// }); +// test('handleChange should update the OTP value and call setOtp with validity false when a valid number is entered', () => { +// const setOtpMock = mock(() => {}); +// const { getAllByRole } = render(); +// const inputs = getAllByRole('textbox'); +// const inputIndex = 0; +// const inputValue = '1'; -// test('handleChange should update the OTP value and call setOtp with validity false when a valid number is entered', async () => { -// const setOtpMock = mock(() => {}); -// const { getAllByRole } = render(); -// const inputs = getAllByRole('textbox'); -// const inputIndex = 0; -// const inputValue = '1'; - -// await act(async () => { -// fireEvent.change(inputs[inputIndex], { target: { value: inputValue } }); -// }); - -// expect(setOtpMock).toHaveBeenCalled(); -// expect(setOtpMock).toHaveBeenCalledWith(['1', '', '', '', '', ''], false); -// }); - - - +// fireEvent.change(inputs[inputIndex], { target: { value: inputValue } }); -// test('handleChange should update the OTP value and call setOtp with validity false when a valid number is entered', () => { -// const setOtpMock = mock(() => {}); -// const { getAllByRole } = render(); -// const inputs = getAllByRole('textbox'); -// const inputIndex = 0; -// const inputValue = '0'; - -// fireEvent.change(inputs[inputIndex], { target: { value: inputValue } }); - -// // Check if the mock function was called -// expect(setOtpMock).toHaveBeenCalled(); - -// // Check if the mock function was called with the expected arguments -// const expectedOtp = ['1', '', '', '', '', '']; -// const expectedValidity = false; -// expect(setOtpMock).toHaveBeenCalledWith(expectedOtp, expectedValidity); -// }); +// // Check the mock function's calls to verify it was called with the expected arguments +// expect(setOtpMock.mock.calls).toContainEqual([expect.arrayContaining([inputValue]), false]); +// }); // test('handleChange should update the OTP value and call setOtp with validity true when the last digit is entered', () => { // const setOtpMock = mock(() => {}); diff --git a/frontend/occupi-web/src/components/OtpComponent/OtpComponent.tsx b/frontend/occupi-web/src/components/OtpComponent/OtpComponent.tsx index 5ebd2696..272ca262 100644 --- a/frontend/occupi-web/src/components/OtpComponent/OtpComponent.tsx +++ b/frontend/occupi-web/src/components/OtpComponent/OtpComponent.tsx @@ -1,4 +1,5 @@ -import React, { useState, useRef } from 'react' + +import React,{useState, useRef} from 'react' type OtpComponentProps = { setOtp: (otp: string[], validity: boolean) => void; @@ -38,37 +39,32 @@ const OtpComponent = (props: OtpComponentProps) => { if (index > 0) { inputsRef.current[index - 1]?.focus(); props.setOtp(otp, false); + } } }; return ( -
+
{err !== "" &&
{err}
} -
- {otp.map((data, index) => ( - handleChange(e.target, index)} - onKeyDown={(e) => handleKeyDown(e, index)} - ref={(element) => inputsRef.current[index] = element} - className={` - w-[10vw] h-[10vw] - min-w-[40px] min-h-[40px] - max-w-[60px] max-h-[60px] - rounded-[15px] bg-secondary - p-[8px] mb-[10px] mt-6 text-center - ${err !== "" ? "border-[2px] border-red_salmon" : ""} - `} - /> - ))} -
+ {otp.map((data, index) => ( + handleChange(e.target, index)} + onKeyDown={(e) => handleKeyDown(e, index)} + ref={(element) => inputsRef.current[index] = element} + className={'h-[3.48vw] w-[3.48vw] rounded-[15px] bg-secondary p-[8px] mb-[5px] mt-6 text-center ' + + (index !== 5 ? " mr-[1.81vw]" : "") + + (err !== "" ? " border-[2px] border-red_salmon " : "") + } + /> + ))}
); } -export default OtpComponent \ No newline at end of file +export default OtpComponent diff --git a/frontend/occupi-web/src/components/OverviewComponent/Overview.test.tsx b/frontend/occupi-web/src/components/OverviewComponent/Overview.test.tsx index 1d6e153b..218063e7 100644 --- a/frontend/occupi-web/src/components/OverviewComponent/Overview.test.tsx +++ b/frontend/occupi-web/src/components/OverviewComponent/Overview.test.tsx @@ -1,27 +1,25 @@ import { describe, expect, test } from "bun:test"; import { render, screen } from "@testing-library/react"; import OverviewComponent from "./OverviewComponent"; -import { UserProvider } from "UserContext"; // Import the UserProvider -// Create a wrapper component that provides the UserContext -import { ReactNode } from "react"; +describe("OverviewComponent Tests", () => { + test("renders greeting and welcome messages", () => { + render(); + expect(screen.getByText("Hi Tina πŸ‘‹")).toBeTruthy(); // Checks if the greeting text is rendered + expect(screen.getByText("Welcome to Occupi")).toBeTruthy(); // Checks if the welcome message is rendered + }); -const Wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - -); -describe("OverviewComponent Tests", () => { - // test("renders greeting and welcome messages", () => { - // render(, { wrapper: Wrapper }); - // // expect(screen.getByText("Hi Tina πŸ‘‹")).toBeTruthy(); - // expect(screen.getByText("Welcome to Occupi")).toBeTruthy(); - // }); test("renders images and checks their presence", () => { - render(, { wrapper: Wrapper }); + render(); const images = screen.getAllByRole("img"); - expect(images.length).toBeGreaterThan(0); + expect(images.length).toBeGreaterThan(0); // Checks if there are any images rendered }); -}); \ No newline at end of file + + + + +}); + + diff --git a/frontend/occupi-web/src/components/ProfileComponent/ProfileComponent.test.tsx b/frontend/occupi-web/src/components/ProfileComponent/ProfileComponent.test.tsx index 84e51b5d..0015d773 100644 --- a/frontend/occupi-web/src/components/ProfileComponent/ProfileComponent.test.tsx +++ b/frontend/occupi-web/src/components/ProfileComponent/ProfileComponent.test.tsx @@ -14,12 +14,20 @@ describe("ProfileComponent Simple Tests", () => { describe("ProfileComponent", () => { - test("renders profile image", () => { - render(); - const imgs = screen.getAllByRole("img"); - expect(imgs.length).toBeGreaterThan(0); - }); + // test("renders profile image", () => { + // render(); + // const imgs = screen.getAllByRole("img"); + // expect(imgs.length).toBeGreaterThan(0); + // expect(imgs[0].getAttribute("src")).toBe("https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg"); + // }); + + // test("renders with default props when not provided", () => { + // render(); + // const imgs = screen.getAllByRole("img"); + // expect(imgs.length).toBeGreaterThan(0); + // expect(imgs[0].getAttribute("src")).toBe("https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg"); + // }); test("renders avatar with online status", () => { render(); diff --git a/frontend/occupi-web/src/components/ProfileComponent/ProfileComponent.tsx b/frontend/occupi-web/src/components/ProfileComponent/ProfileComponent.tsx index 61533738..423c519a 100644 --- a/frontend/occupi-web/src/components/ProfileComponent/ProfileComponent.tsx +++ b/frontend/occupi-web/src/components/ProfileComponent/ProfileComponent.tsx @@ -4,27 +4,27 @@ type ProfileComponentProps = { profileImage?: string; email?: string; name?: string; - officeStatus?: "onsite" | "offsite" | "booked"; + officeStatus?: "in" | "out" | "booked"; }; const ProfileComponent = ({ profileImage = "https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg", email = "defaultEmail@example.com", name = "Janet Doey", - officeStatus = "onsite", + officeStatus = "in", }: ProfileComponentProps) => { - const getStatusBadge = (status: "onsite" | "offsite" | "booked") => { + const getStatusBadge = (status: "in" | "out" | "booked") => { let colorClass: string; let text: string; switch (status) { - case "onsite": + case "in": colorClass = "badge-success"; text = "In Office"; break; - case "offsite": + case "out": colorClass = "badge-error"; - text = "offsite of Office"; + text = "Out of Office"; break; case "booked": colorClass = "badge-warning"; diff --git a/frontend/occupi-web/src/components/SideNavBarButton/SideNavBarButton.tsx b/frontend/occupi-web/src/components/SideNavBarButton/SideNavBarButton.tsx index e992b7e1..330adfc6 100644 --- a/frontend/occupi-web/src/components/SideNavBarButton/SideNavBarButton.tsx +++ b/frontend/occupi-web/src/components/SideNavBarButton/SideNavBarButton.tsx @@ -2,7 +2,7 @@ import { motion } from "framer-motion"; const variants = { open: { - width: "95%", + width: "12.5vw", }, closed: { width: "40px", diff --git a/frontend/occupi-web/src/components/appearance/Appearance.test.tsx b/frontend/occupi-web/src/components/appearance/Appearance.test.tsx index cfa9ab8d..00080de4 100644 --- a/frontend/occupi-web/src/components/appearance/Appearance.test.tsx +++ b/frontend/occupi-web/src/components/appearance/Appearance.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +// import { test, expect } from 'bun:test'; import Appearance from './Appearance'; test('Appearance Component changes theme to dark', async () => { diff --git a/frontend/occupi-web/src/components/bookingComponent/BookingComponent.tsx b/frontend/occupi-web/src/components/bookingComponent/BookingComponent.tsx index d6480016..619439a8 100644 --- a/frontend/occupi-web/src/components/bookingComponent/BookingComponent.tsx +++ b/frontend/occupi-web/src/components/bookingComponent/BookingComponent.tsx @@ -153,10 +153,7 @@ export default function App() { {cellValue} ); - - case "actions": - return (
@@ -166,7 +163,7 @@ export default function App() { {/* Hello */}
- + {/* @@ -177,7 +174,7 @@ export default function App() { diff --git a/frontend/occupi-web/src/components/bookings/Bookings.tsx b/frontend/occupi-web/src/components/bookings/Bookings.tsx index fd0756b0..f25a3f58 100644 --- a/frontend/occupi-web/src/components/bookings/Bookings.tsx +++ b/frontend/occupi-web/src/components/bookings/Bookings.tsx @@ -1,329 +1,329 @@ -// import React from "react"; -// import { -// Table, -// TableHeader, -// TableColumn, -// TableBody, -// TableRow, -// TableCell, -// Input, -// Button, -// DropdownTrigger, -// Dropdown, -// DropdownMenu, -// DropdownItem, -// Chip, -// User, -// Pagination, -// Selection, -// ChipProps, -// SortDescriptor -// } from "@nextui-org/react"; +import React from "react"; +import { + Table, + TableHeader, + TableColumn, + TableBody, + TableRow, + TableCell, + Input, + Button, + DropdownTrigger, + Dropdown, + DropdownMenu, + DropdownItem, + Chip, + User, + Pagination, + Selection, + ChipProps, + SortDescriptor +} from "@nextui-org/react"; -// import { columns, users, statusOptions } from "../data/Data"; -// import { capitalize } from "../data/Utils"; -// import { FaPlus, FaEllipsisV, FaChevronDown, FaSearch } from 'react-icons/fa'; +import { columns, users, statusOptions } from "../data/Data"; +import { capitalize } from "../data/Utils"; +import { FaPlus, FaEllipsisV, FaChevronDown, FaSearch } from 'react-icons/fa'; -// import { Key } from "@react-types/shared"; -// const statusColorMap: Record = { -// ONSITE: "success", -// BOOKED: "warning", -// OFFSITE: "danger", -// }; +import { Key } from "@react-types/shared"; +const statusColorMap: Record = { + ONSITE: "success", + BOOKED: "warning", + OFFSITE: "danger", +}; -// const INITIAL_VISIBLE_COLUMNS = ["name", "role", "status", "actions"]; +const INITIAL_VISIBLE_COLUMNS = ["name", "role", "status", "actions"]; -// type User = typeof users[0]; +type User = typeof users[0]; -// export default function Bookings() { -// const [filterValue, setFilterValue] = React.useState(""); -// const [selectedKeys, setSelectedKeys] = React.useState(new Set([])); -// const [visibleColumns, setVisibleColumns] = React.useState(new Set(INITIAL_VISIBLE_COLUMNS)); -// const [statusFilter, setStatusFilter] = React.useState("all"); -// const [rowsPerPage, setRowsPerPage] = React.useState(5); -// const [sortDescriptor, setSortDescriptor] = React.useState({ -// column: "age", -// direction: "ascending", -// }); +export default function Bookings() { + const [filterValue, setFilterValue] = React.useState(""); + const [selectedKeys, setSelectedKeys] = React.useState(new Set([])); + const [visibleColumns, setVisibleColumns] = React.useState(new Set(INITIAL_VISIBLE_COLUMNS)); + const [statusFilter, setStatusFilter] = React.useState("all"); + const [rowsPerPage, setRowsPerPage] = React.useState(5); + const [sortDescriptor, setSortDescriptor] = React.useState({ + column: "age", + direction: "ascending", + }); -// const [page, setPage] = React.useState(1); + const [page, setPage] = React.useState(1); -// const hasSearchFilter = Boolean(filterValue); + const hasSearchFilter = Boolean(filterValue); -// const headerColumns = React.useMemo(() => { -// if (visibleColumns === "all") return columns; + const headerColumns = React.useMemo(() => { + if (visibleColumns === "all") return columns; -// return columns.filter((column: { uid: Key }) => Array.from(visibleColumns).includes(column.uid)); -// }, [visibleColumns]); + return columns.filter((column: { uid: Key }) => Array.from(visibleColumns).includes(column.uid)); + }, [visibleColumns]); -// const filteredItems = React.useMemo(() => { -// let filteredUsers = [...users]; + const filteredItems = React.useMemo(() => { + let filteredUsers = [...users]; -// if (hasSearchFilter) { -// filteredUsers = filteredUsers.filter((user) => -// user.name.toLowerCase().includes(filterValue.toLowerCase()), -// ); -// } -// if (statusFilter !== "all" && Array.from(statusFilter).length !== statusOptions.length) { -// filteredUsers = filteredUsers.filter((user) => -// Array.from(statusFilter).includes(user.status), -// ); -// } + if (hasSearchFilter) { + filteredUsers = filteredUsers.filter((user) => + user.name.toLowerCase().includes(filterValue.toLowerCase()), + ); + } + if (statusFilter !== "all" && Array.from(statusFilter).length !== statusOptions.length) { + filteredUsers = filteredUsers.filter((user) => + Array.from(statusFilter).includes(user.status), + ); + } -// return filteredUsers; -// }, [users, filterValue, statusFilter]); + return filteredUsers; + }, [users, filterValue, statusFilter]); -// const pages = Math.ceil(filteredItems.length / rowsPerPage); + const pages = Math.ceil(filteredItems.length / rowsPerPage); -// const items = React.useMemo(() => { -// const start = (page - 1) * rowsPerPage; -// const end = start + rowsPerPage; + const items = React.useMemo(() => { + const start = (page - 1) * rowsPerPage; + const end = start + rowsPerPage; -// return filteredItems.slice(start, end); -// }, [page, filteredItems, rowsPerPage]); + return filteredItems.slice(start, end); + }, [page, filteredItems, rowsPerPage]); -// const sortedItems = React.useMemo(() => { -// return [...items].sort((a: User, b: User) => { -// const first = a[sortDescriptor.column as keyof User] as number; -// const second = b[sortDescriptor.column as keyof User] as number; -// const cmp = first < second ? -1 : first > second ? 1 : 0; + const sortedItems = React.useMemo(() => { + return [...items].sort((a: User, b: User) => { + const first = a[sortDescriptor.column as keyof User] as number; + const second = b[sortDescriptor.column as keyof User] as number; + const cmp = first < second ? -1 : first > second ? 1 : 0; -// return sortDescriptor.direction === "descending" ? -cmp : cmp; -// }); -// }, [sortDescriptor, items]); + return sortDescriptor.direction === "descending" ? -cmp : cmp; + }); + }, [sortDescriptor, items]); -// const renderCell = React.useCallback((user: User, columnKey: React.Key) => { -// const cellValue = user[columnKey as keyof User]; + const renderCell = React.useCallback((user: User, columnKey: React.Key) => { + const cellValue = user[columnKey as keyof User]; -// switch (columnKey) { -// case "name": -// return ( -// -// {user.email} -// -// ); -// case "role": -// return ( -//
-//

{cellValue}

-//

{user.team}

-//
-// ); -// case "status": -// return ( -// -// {cellValue} -// -// ); -// case "actions": -// return ( -//
-// -// -// -// -// -// View -// Edit -// Delete -// -// -//
-// ); -// default: -// return cellValue; -// } -// }, []); + switch (columnKey) { + case "name": + return ( + + {user.email} + + ); + case "role": + return ( +
+

{cellValue}

+

{user.team}

+
+ ); + case "status": + return ( + + {cellValue} + + ); + case "actions": + return ( +
+ + + + + + View + Edit + Delete + + +
+ ); + default: + return cellValue; + } + }, []); -// const onNextPage = React.useCallback(() => { -// if (page < pages) { -// setPage(page + 1); -// } -// }, [page, pages]); + const onNextPage = React.useCallback(() => { + if (page < pages) { + setPage(page + 1); + } + }, [page, pages]); -// const onPreviousPage = React.useCallback(() => { -// if (page > 1) { -// setPage(page - 1); -// } -// }, [page]); + const onPreviousPage = React.useCallback(() => { + if (page > 1) { + setPage(page - 1); + } + }, [page]); -// const onRowsPerPageChange = React.useCallback((e: React.ChangeEvent) => { -// setRowsPerPage(Number(e.target.value)); -// setPage(1); -// }, []); + const onRowsPerPageChange = React.useCallback((e: React.ChangeEvent) => { + setRowsPerPage(Number(e.target.value)); + setPage(1); + }, []); -// const onSearchChange = React.useCallback((value?: string) => { -// if (value) { -// setFilterValue(value); -// setPage(1); -// } else { -// setFilterValue(""); -// } -// }, []); + const onSearchChange = React.useCallback((value?: string) => { + if (value) { + setFilterValue(value); + setPage(1); + } else { + setFilterValue(""); + } + }, []); -// const onClear = React.useCallback(()=>{ -// setFilterValue("") -// setPage(1) -// },[]) + const onClear = React.useCallback(()=>{ + setFilterValue("") + setPage(1) + },[]) -// const topContent = React.useMemo(() => { -// return ( + const topContent = React.useMemo(() => { + return ( -//
-//
-// } -// value={filterValue} -// onClear={() => onClear()} -// onValueChange={onSearchChange} -// /> -//
-// -// -// -// -// -// {statusOptions.map((status: { uid: string; name: string; }) => ( -// -// {capitalize(status.name)} -// -// ))} -// -// -// -// -// -// -// -// {columns.map((column: { uid: string; name: string; }) => ( -// -// {capitalize(column.name)} -// -// ))} -// -// -// -//
-//
-//
-// Total {users.length} users -// -//
-//
-// ); -// }, [ -// filterValue, -// statusFilter, -// visibleColumns, -// onSearchChange, -// onRowsPerPageChange, -// users.length, -// hasSearchFilter, -// ]); +
+
+ } + value={filterValue} + onClear={() => onClear()} + onValueChange={onSearchChange} + /> +
+ + + + + + {statusOptions.map((status: { uid: string; name: string; }) => ( + + {capitalize(status.name)} + + ))} + + + + + + + + {columns.map((column: { uid: string; name: string; }) => ( + + {capitalize(column.name)} + + ))} + + + +
+
+
+ Total {users.length} users + +
+
+ ); + }, [ + filterValue, + statusFilter, + visibleColumns, + onSearchChange, + onRowsPerPageChange, + users.length, + hasSearchFilter, + ]); -// const bottomContent = React.useMemo(() => { -// return ( -//
-// -// {selectedKeys === "all" -// ? "All items selected" -// : `${selectedKeys.size} of ${filteredItems.length} selected`} -// -// -//
-// -// -//
-//
-// ); -// }, [selectedKeys, items.length, page, pages, hasSearchFilter]); + const bottomContent = React.useMemo(() => { + return ( +
+ + {selectedKeys === "all" + ? "All items selected" + : `${selectedKeys.size} of ${filteredItems.length} selected`} + + +
+ + +
+
+ ); + }, [selectedKeys, items.length, page, pages, hasSearchFilter]); -// return ( -// -// -// {(column: { uid: string; sortable: boolean; name: string; }) => ( -// -// {column.name} -// -// )} -// -// -// {(item: { id: number; name: string; role: string; team: string; status: string; age: string; avatar: string; email: string; bookings: number; }) => ( -// -// {(columnKey: React.Key) => {renderCell(item, columnKey)}} -// -// )} -// -//
-// ); -// } + return ( + + + {(column: { uid: string; sortable: boolean; name: string; }) => ( + + {column.name} + + )} + + + {(item: { id: number; name: string; role: string; team: string; status: string; age: string; avatar: string; email: string; bookings: number; }) => ( + + {(columnKey: React.Key) => {renderCell(item, columnKey)}} + + )} + +
+ ); +} diff --git a/frontend/occupi-web/src/components/data/Data.test.tsx b/frontend/occupi-web/src/components/data/Data.test.tsx index 1d3727dc..71a82065 100644 --- a/frontend/occupi-web/src/components/data/Data.test.tsx +++ b/frontend/occupi-web/src/components/data/Data.test.tsx @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { columns, statusOptions, fetchUsers } from "./Data"; // Adjust the import path as necessary +import { columns, users, statusOptions } from "./Data"; // Adjust the import path as necessary // Test the structure and integrity of columns test("columns array structure and properties", () => { @@ -30,45 +30,9 @@ test("columns array structure and properties", () => { expect(nonSortableColumns).toHaveLength(3); }); // Test the integrity and structure of users -// test("users array has the correct structure and content", () => { -// expect(users.length).toBe(20); -// users.forEach((user: { id: string; name: string; role: string; team: string; status:string; email:string; bookings: number; }) => { -// expect(user).toHaveProperty('id'); -// expect(user).toHaveProperty('name'); -// expect(user).toHaveProperty('role'); -// expect(user).toHaveProperty('team'); -// expect(user).toHaveProperty('status'); -// expect(user).toHaveProperty('email'); -// expect(user).toHaveProperty('bookings'); -// expect(typeof user.id).toBe('number'); -// expect(typeof user.name).toBe('string'); -// expect(typeof user.role).toBe('string'); -// expect(typeof user.team).toBe('string'); -// expect(typeof user.status).toBe('string'); -// expect(typeof user.email).toBe('string'); -// expect(typeof user.bookings).toBe('number'); -// }); -// }); - -// Test the integrity and structure of statusOptions -test("statusOptions array has the correct entries", () => { - expect(statusOptions.length).toBe(3); - statusOptions.forEach((option: { name: string; uid: string; }) => { - expect(option).toHaveProperty('name'); - expect(option).toHaveProperty('uid'); - expect(typeof option.name).toBe('string'); - expect(typeof option.uid).toBe('string'); - }); -}); - - - -// Test the integrity and structure of users -test("users array has the correct structure and content 2", async () => { - const users = await fetchUsers(); - - expect(users.length).toBeGreaterThanOrEqual(0); - users.forEach((user) => { +test("users array has the correct structure and content", () => { + expect(users.length).toBe(20); + users.forEach((user: { id: number; name: string; role: string; team: string; status:string; email:string; bookings: number; }) => { expect(user).toHaveProperty('id'); expect(user).toHaveProperty('name'); expect(user).toHaveProperty('role'); @@ -76,7 +40,7 @@ test("users array has the correct structure and content 2", async () => { expect(user).toHaveProperty('status'); expect(user).toHaveProperty('email'); expect(user).toHaveProperty('bookings'); - expect(typeof user.id).toBe('string'); + expect(typeof user.id).toBe('number'); expect(typeof user.name).toBe('string'); expect(typeof user.role).toBe('string'); expect(typeof user.team).toBe('string'); @@ -86,3 +50,13 @@ test("users array has the correct structure and content 2", async () => { }); }); +// Test the integrity and structure of statusOptions +test("statusOptions array has the correct entries", () => { + expect(statusOptions.length).toBe(3); + statusOptions.forEach((option: { name: string; uid: string; }) => { + expect(option).toHaveProperty('name'); + expect(option).toHaveProperty('uid'); + expect(typeof option.name).toBe('string'); + expect(typeof option.uid).toBe('string'); + }); +}); \ No newline at end of file diff --git a/frontend/occupi-web/src/components/data/Data.tsx b/frontend/occupi-web/src/components/data/Data.tsx index 8d04baff..aa28adf1 100644 --- a/frontend/occupi-web/src/components/data/Data.tsx +++ b/frontend/occupi-web/src/components/data/Data.tsx @@ -1,6 +1,3 @@ -import axios from 'axios'; - - const columns = [ {name: "OCCUPI-ID", uid: "id", sortable: true}, {name: "NAME", uid: "name", sortable: true}, @@ -19,366 +16,227 @@ const statusOptions = [ {name: "OFFSITE", uid: "OFFSITE"}, ]; +const users = [ + { + id: 1, + name: "Tony Reichert", + role: "CEO", + team: "Management", + status: "ONSITE", + age: "29", + avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d", + email: "tony.reichert@example.com", + bookings: 3, + }, + { + id: 2, + name: "Zoey Lang", + role: "Tech Lead", + team: "Development", + status: "BOOKED", + age: "25", + avatar: "https://i.pravatar.cc/150?u=a042581f4e29026704d", + email: "zoey.lang@example.com", + bookings: 2, + }, + { + id: 3, + name: "Jane Fisher", + role: "Sr. Dev", + team: "Development", + status: "ONSITE", + age: "22", + avatar: "https://i.pravatar.cc/150?u=a04258114e29026702d", + email: "jane.fisher@example.com", + bookings: 1, + }, + { + id: 4, + name: "William Howard", + role: "C.M.", + team: "Marketing", + status: "OFFSITE", + age: "28", + avatar: "https://i.pravatar.cc/150?u=a048581f4e29026701d", + email: "william.howard@example.com", + bookings: 0, + }, + { + id: 5, + name: "Kristen Copper", + role: "S. Manager", + team: "Sales", + status: "ONSITE", + age: "24", + avatar: "https://i.pravatar.cc/150?u=a092581d4ef9026700d", + email: "kristen.cooper@example.com", + bookings: 2, + }, + { + id: 6, + name: "Brian Kim", + role: "P. Manager", + team: "Management", + age: "29", + avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d", + email: "brian.kim@example.com", + status: "ONSITE", + bookings: 3, + }, + { + id: 7, + name: "Michael Hunt", + role: "Designer", + team: "Design", + status: "BOOKED", + age: "27", + avatar: "https://i.pravatar.cc/150?u=a042581f4e29027007d", + email: "michael.hunt@example.com", + bookings: 1, + }, + { + id: 8, + name: "Samantha Brooks", + role: "HR Manager", + team: "HR", + status: "ONSITE", + age: "31", + avatar: "https://i.pravatar.cc/150?u=a042581f4e27027008d", + email: "samantha.brooks@example.com", + bookings: 4, + }, + { + id: 9, + name: "Frank Harrison", + role: "F. Manager", + team: "Finance", + status: "OFFSITE", + age: "33", + avatar: "https://i.pravatar.cc/150?img=4", + email: "frank.harrison@example.com", + bookings: 0, + }, + { + id: 10, + name: "Emma Adams", + role: "Ops Manager", + team: "Operations", + status: "ONSITE", + age: "35", + avatar: "https://i.pravatar.cc/150?img=5", + email: "emma.adams@example.com", + bookings: 2, + }, + { + id: 11, + name: "Brandon Stevens", + role: "Jr. Dev", + team: "Development", + status: "ONSITE", + age: "22", + avatar: "https://i.pravatar.cc/150?img=8", + email: "brandon.stevens@example.com", + bookings: 1, + }, + { + id: 12, + name: "Megan Richards", + role: "P. Manager", + team: "Product", + status: "BOOKED", + age: "28", + avatar: "https://i.pravatar.cc/150?img=10", + email: "megan.richards@example.com", + bookings: 1, + }, + { + id: 13, + name: "Oliver Scott", + role: "S. Manager", + team: "Security", + status: "ONSITE", + age: "37", + avatar: "https://i.pravatar.cc/150?img=12", + email: "oliver.scott@example.com", + bookings: 3, + }, + { + id: 14, + name: "Grace Allen", + role: "M. Specialist", + team: "Marketing", + status: "ONSITE", + age: "30", + avatar: "https://i.pravatar.cc/150?img=16", + email: "grace.allen@example.com", + bookings: 2, + }, + { + id: 15, + name: "Noah Carter", + role: "IT Specialist", + team: "I. Technology", + status: "BOOKED", + age: "31", + avatar: "https://i.pravatar.cc/150?img=15", + email: "noah.carter@example.com", + bookings: 1, + }, + { + id: 16, + name: "Ava Perez", + role: "Manager", + team: "Sales", + status: "ONSITE", + age: "29", + avatar: "https://i.pravatar.cc/150?img=20", + email: "ava.perez@example.com", + bookings: 3, + }, + { + id: 17, + name: "Liam Johnson", + role: "Data Analyst", + team: "Analysis", + status: "ONSITE", + age: "28", + avatar: "https://i.pravatar.cc/150?img=33", + email: "liam.johnson@example.com", + bookings: 1, + }, + { + id: 18, + name: "Sophia Taylor", + role: "QA Analyst", + team: "Testing", + status: "ONSITE", + age: "27", + avatar: "https://i.pravatar.cc/150?img=29", + email: "sophia.taylor@example.com", + bookings: 2, + }, + { + id: 19, + name: "Lucas Harris", + role: "Administrator", + team: "Information Technology", + status: "BOOKED", + age: "32", + avatar: "https://i.pravatar.cc/150?img=50", + email: "lucas.harris@example.com", + bookings: 1, + }, + { + id: 20, + name: "Mia Robinson", + role: "Coordinator", + team: "Operations", + status: "ONSITE", + age: "26", + avatar: "https://i.pravatar.cc/150?img=45", + email: "mia.robinson@example.com", + bookings: 3, + }, +]; -interface ApiUser { - _id: string; - occupiId: string; - details?: { - name: string; - }; - email: string; - position?: string; - departmentNo?: string; - status?: string; - onSite: boolean; -} - -interface ApiResponse { - data: ApiUser[]; - meta: { - currentPage: number; - totalPages: number; - totalResults: number; - }; - status: number; -} - -interface User { - id: string; - name: string; - role: string; - team: string; - status: string; - email: string; - bookings: number; - avatar: string; -} - - - - -let users: User[] = []; - -// useEffect(() => { -// fetchUsers(); -// }, []); - - -const fetchUsers = async (): Promise => { - try { - const response = await axios.get('/api/get-users'); - - const fetchedUsers = response.data.data.map(user => ({ - id: user.occupiId, - name: user.details?.name || 'N/A', - role: user.position || 'N/A', - team: user.departmentNo || 'N/A', - status: user.onSite ? 'ONSITE' : (user.status || 'OFFSITE'), - email: user.email, - bookings: Math.floor(Math.random() * 5), - avatar: `https://i.pravatar.cc/150?u=${user.occupiId}`, // Generate a random avatar - })); - - users = fetchedUsers; // Update the users array - return fetchedUsers; - } catch (err) { - console.error('Failed to fetch users:', err); - return []; - } -}; - -// Function to initialize users -const initUsers = async () => { - users = await fetchUsers(); -}; - -// Call initUsers when your application starts -initUsers(); - -export { columns, statusOptions, users, fetchUsers }; - - - - - - - - - - - - - - -// const users = [ -// { -// id: 1, -// name: "Tony Reichert", -// role: "CEO", -// team: "Management", -// status: "ONSITE", -// age: "29", -// avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d", -// email: "tony.reichert@example.com", -// bookings: 3, -// }, -// { -// id: 2, -// name: "Zoey Lang", -// role: "Tech Lead", -// team: "Development", -// status: "BOOKED", -// age: "25", -// avatar: "https://i.pravatar.cc/150?u=a042581f4e29026704d", -// email: "zoey.lang@example.com", -// bookings: 2, -// }, -// { -// id: 3, -// name: "Jane Fisher", -// role: "Sr. Dev", -// team: "Development", -// status: "ONSITE", -// age: "22", -// avatar: "https://i.pravatar.cc/150?u=a04258114e29026702d", -// email: "jane.fisher@example.com", -// bookings: 1, -// }, -// { -// id: 4, -// name: "William Howard", -// role: "C.M.", -// team: "Marketing", -// status: "OFFSITE", -// age: "28", -// avatar: "https://i.pravatar.cc/150?u=a048581f4e29026701d", -// email: "william.howard@example.com", -// bookings: 0, -// }, -// { -// id: 5, -// name: "Kristen Copper", -// role: "S. Manager", -// team: "Sales", -// status: "ONSITE", -// age: "24", -// avatar: "https://i.pravatar.cc/150?u=a092581d4ef9026700d", -// email: "kristen.cooper@example.com", -// bookings: 2, -// }, -// { -// id: 6, -// name: "Brian Kim", -// role: "P. Manager", -// team: "Management", -// age: "29", -// avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d", -// email: "brian.kim@example.com", -// status: "ONSITE", -// bookings: 3, -// }, -// { -// id: 7, -// name: "Michael Hunt", -// role: "Designer", -// team: "Design", -// status: "BOOKED", -// age: "27", -// avatar: "https://i.pravatar.cc/150?u=a042581f4e29027007d", -// email: "michael.hunt@example.com", -// bookings: 1, -// }, -// { -// id: 8, -// name: "Samantha Brooks", -// role: "HR Manager", -// team: "HR", -// status: "ONSITE", -// age: "31", -// avatar: "https://i.pravatar.cc/150?u=a042581f4e27027008d", -// email: "samantha.brooks@example.com", -// bookings: 4, -// }, -// { -// id: 9, -// name: "Frank Harrison", -// role: "F. Manager", -// team: "Finance", -// status: "OFFSITE", -// age: "33", -// avatar: "https://i.pravatar.cc/150?img=4", -// email: "frank.harrison@example.com", -// bookings: 0, -// }, -// { -// id: 10, -// name: "Emma Adams", -// role: "Ops Manager", -// team: "Operations", -// status: "ONSITE", -// age: "35", -// avatar: "https://i.pravatar.cc/150?img=5", -// email: "emma.adams@example.com", -// bookings: 2, -// }, -// { -// id: 11, -// name: "Brandon Stevens", -// role: "Jr. Dev", -// team: "Development", -// status: "ONSITE", -// age: "22", -// avatar: "https://i.pravatar.cc/150?img=8", -// email: "brandon.stevens@example.com", -// bookings: 1, -// }, -// { -// id: 12, -// name: "Megan Richards", -// role: "P. Manager", -// team: "Product", -// status: "BOOKED", -// age: "28", -// avatar: "https://i.pravatar.cc/150?img=10", -// email: "megan.richards@example.com", -// bookings: 1, -// }, -// { -// id: 13, -// name: "Oliver Scott", -// role: "S. Manager", -// team: "Security", -// status: "ONSITE", -// age: "37", -// avatar: "https://i.pravatar.cc/150?img=12", -// email: "oliver.scott@example.com", -// bookings: 3, -// }, -// { -// id: 14, -// name: "Grace Allen", -// role: "M. Specialist", -// team: "Marketing", -// status: "ONSITE", -// age: "30", -// avatar: "https://i.pravatar.cc/150?img=16", -// email: "grace.allen@example.com", -// bookings: 2, -// }, -// { -// id: 15, -// name: "Noah Carter", -// role: "IT Specialist", -// team: "I. Technology", -// status: "BOOKED", -// age: "31", -// avatar: "https://i.pravatar.cc/150?img=15", -// email: "noah.carter@example.com", -// bookings: 1, -// }, -// { -// id: 16, -// name: "Ava Perez", -// role: "Manager", -// team: "Sales", -// status: "ONSITE", -// age: "29", -// avatar: "https://i.pravatar.cc/150?img=20", -// email: "ava.perez@example.com", -// bookings: 3, -// }, -// { -// id: 17, -// name: "Liam Johnson", -// role: "Data Analyst", -// team: "Analysis", -// status: "ONSITE", -// age: "28", -// avatar: "https://i.pravatar.cc/150?img=33", -// email: "liam.johnson@example.com", -// bookings: 1, -// }, -// { -// id: 18, -// name: "Sophia Taylor", -// role: "QA Analyst", -// team: "Testing", -// status: "ONSITE", -// age: "27", -// avatar: "https://i.pravatar.cc/150?img=29", -// email: "sophia.taylor@example.com", -// bookings: 2, -// }, -// { -// id: 19, -// name: "Lucas Harris", -// role: "Administrator", -// team: "Information Technology", -// status: "BOOKED", -// age: "32", -// avatar: "https://i.pravatar.cc/150?img=50", -// email: "lucas.harris@example.com", -// bookings: 1, -// }, -// { -// id: 20, -// name: "Mia Robinson", -// role: "Coordinator", -// team: "Operations", -// status: "ONSITE", -// age: "26", -// avatar: "https://i.pravatar.cc/150?img=45", -// email: "mia.robinson@example.com", -// bookings: 3, -// }, -// ]; - -// export {columns, users, statusOptions}; - - -// import React, { useEffect, useState } from 'react'; -// import DataService, { User } from '../../DataService'; - -// // Your component - -// const [users, setUsers] = useState([]); -// const [loading, setLoading] = useState(true); -// const [error, setError] = useState(null); - -// useEffect(() => { -// fetchUsers(); -// }, []); - -// const fetchUsers = async () => { -// try { -// setLoading(true); -// const fetchedUsers = await DataService.fetchUsers(); -// setUsers(fetchedUsers); -// setLoading(false); -// } catch (err) { -// setError('Failed to fetch users'); -// setLoading(false); -// } -// }; - -// // Keep the columns and statusOptions as they are -// const columns = [ -// {name: "OCCUPI-ID", uid: "id", sortable: true}, -// {name: "NAME", uid: "name", sortable: true}, -// {name: "ROLE", uid: "role", sortable: true}, -// {name: "DEPARTMENT", uid: "team"}, -// {name: "EMAIL", uid: "email"}, -// {name: "STATUS", uid: "status", sortable: true}, -// {name: "ACTIONS", uid: "actions"}, -// {name:"BOOKINGS THIS WEEK", uid:"bookings", sortable: true}, -// ]; - -// const statusOptions = [ -// {name: "ONSITE", uid: "ONSITE"}, -// {name: "BOOKED", uid: "BOOKED"}, -// {name: "OFFSITE", uid: "OFFSITE"}, -// ]; - - -// export {columns, users, statusOptions}; - +export {columns, users, statusOptions}; \ No newline at end of file diff --git a/frontend/occupi-web/src/components/drawerComponent/DrawerComponent.test.tsx b/frontend/occupi-web/src/components/drawerComponent/DrawerComponent.test.tsx index f0575079..c11ee2e8 100644 --- a/frontend/occupi-web/src/components/drawerComponent/DrawerComponent.test.tsx +++ b/frontend/occupi-web/src/components/drawerComponent/DrawerComponent.test.tsx @@ -16,11 +16,7 @@ const renderWithRouter = (component: string | number | boolean | Iterable{component}); }; -test("DrawerComponent renders correctly", () => { - renderWithRouter(); - expect(screen.getByText("Profile")).toBeDefined(); - expect(screen.getByText("Appearance")).toBeDefined(); -}); + test("Navigates correctly when 'Profile' is clicked", () => { const { container } = renderWithRouter(); @@ -30,14 +26,3 @@ test("DrawerComponent renders correctly", () => { expect(container.innerHTML).toContain("Profile"); }); - test("Navigates correctly when 'Appearance' is clicked", () => { - const { container } = renderWithRouter(); - const appearanceButton = screen.getByText("Appearance"); - fireEvent.click(appearanceButton); - expect(container.innerHTML).toContain("Appearance"); - }); - - test("Initial state shows 'Please Select a Setting'", () => { - renderWithRouter(); - expect(screen.getByText("Please Select a Setting")).toBeDefined(); - }); \ No newline at end of file diff --git a/frontend/occupi-web/src/components/drawerComponent/__snapshots__/DrawerComponent.test.tsx.snap b/frontend/occupi-web/src/components/drawerComponent/__snapshots__/DrawerComponent.test.tsx.snap new file mode 100644 index 00000000..e69de29b diff --git a/frontend/occupi-web/src/components/gradientButtonComponent/gradientButton.css b/frontend/occupi-web/src/components/gradientButtonComponent/gradientButton.css new file mode 100644 index 00000000..e69de29b diff --git a/frontend/occupi-web/src/components/headerComponent/Header.tsx b/frontend/occupi-web/src/components/headerComponent/Header.tsx index 4b04f5b2..c2931b37 100644 --- a/frontend/occupi-web/src/components/headerComponent/Header.tsx +++ b/frontend/occupi-web/src/components/headerComponent/Header.tsx @@ -1,30 +1,30 @@ -import { motion } from 'framer-motion'; -import { ChevronRight } from 'lucide-react'; -import { useUser } from 'UserContext'; +import { motion } from "framer-motion"; +import { ChevronRight } from "lucide-react"; interface HeaderComponentProps { greeting?: string; + name?: string; welcomeMessage?: string; actionText?: string; } const HeaderComponent: React.FC = ({ - greeting = 'Hi', - welcomeMessage = 'Welcome to Occupi', - actionText = 'Office bookings', + greeting = "Hi", + name = "Tina", + welcomeMessage = "Welcome to Occupi", + actionText = "Office bookings", }) => { - const { userDetails } = useUser(); - return (
- {greeting} {userDetails?.email ?? 'Guest'} πŸ‘‹ + {greeting} {name} πŸ‘‹
{welcomeMessage}
{actionText} diff --git a/frontend/occupi-web/src/components/index.ts b/frontend/occupi-web/src/components/index.ts index cf5a2255..cc08a5ec 100644 --- a/frontend/occupi-web/src/components/index.ts +++ b/frontend/occupi-web/src/components/index.ts @@ -9,7 +9,7 @@ import GraphContainer from "./graphContainer/GraphContainer"; import TabComponent from "./tabComponent/TabComponent"; import Appearance from "./appearance/Appearance"; import MenuItem from './drawerComponent/menuItem/MenuItem'; -// import Bookings from "./bookings/Bookings"; +import Bookings from "./bookings/Bookings"; import OverView from "./overView/OverView"; import OverviewComponent from "./OverviewComponent/OverviewComponent"; import ProfileComponent from "./ProfileComponent/ProfileComponent"; @@ -27,7 +27,6 @@ import OfficePresent from "./officePresentProbabilityChart/OfficePresent"; import StatCard from "./statCard/StatCard"; import Header from "./headerComponent/Header"; import OccupiLoader from "./occupiLoader/OccupiLoader"; -import ProfileDropDown from "./profileDropDown/ProfileDropDown"; export { DrawerComponent, @@ -41,7 +40,7 @@ export { TabComponent, Appearance, MenuItem, - // Bookings, + Bookings, OverView, OverviewComponent, ProfileComponent, @@ -58,6 +57,5 @@ export { OfficePresent, StatCard, Header, - OccupiLoader, - ProfileDropDown + OccupiLoader } \ No newline at end of file diff --git a/frontend/occupi-web/src/components/keyStats.tsx/KeyStats.test.tsx b/frontend/occupi-web/src/components/keyStats.tsx/KeyStats.test.tsx index 0bb2eb31..2079af20 100644 --- a/frontend/occupi-web/src/components/keyStats.tsx/KeyStats.test.tsx +++ b/frontend/occupi-web/src/components/keyStats.tsx/KeyStats.test.tsx @@ -45,8 +45,8 @@ test("KeyStats has correct CSS classes", () => { test("KeyStats contains correct number of elements", () => { // render(); - //const allElements = screen.getAllByText(/.+/); - //expect(allElements.length).toBe(4); // 1 title + 3 stats + const allElements = screen.getAllByText(/.+/); + expect(allElements.length).toBe(4); // 1 title + 3 stats const statElements = screen.getAllByText(/:/); expect(statElements.length).toBe(3); diff --git a/frontend/occupi-web/src/components/linechart/Line_Chat.test.tsx b/frontend/occupi-web/src/components/linechart/Line_Chat.test.tsx index 333bc9c8..874c2df3 100644 --- a/frontend/occupi-web/src/components/linechart/Line_Chat.test.tsx +++ b/frontend/occupi-web/src/components/linechart/Line_Chat.test.tsx @@ -1,5 +1,5 @@ -import { describe, test, afterEach } from "bun:test"; -import { render, cleanup } from "@testing-library/react"; +import { describe, expect, test, afterEach } from "bun:test"; +import { render, screen, cleanup } from "@testing-library/react"; import { Line_Chart } from "@components/index"; @@ -10,8 +10,8 @@ afterEach(() => { describe("Line Chart Component Tests", () => { test("renders the chart and download button", () => { render(); - //expect(screen.getByText("Download Chart")).toBeTruthy(); // Checks if the button is rendered - //expect(screen.getByRole("button")).toBeTruthy(); // Further verifies the button's presence + expect(screen.getByText("Download Chart")).toBeTruthy(); // Checks if the button is rendered + expect(screen.getByRole("button")).toBeTruthy(); // Further verifies the button's presence }); }); diff --git a/frontend/occupi-web/src/components/modal/Modal.test.tsx b/frontend/occupi-web/src/components/modal/Modal.test.tsx index e69de29b..2356f6ed 100644 --- a/frontend/occupi-web/src/components/modal/Modal.test.tsx +++ b/frontend/occupi-web/src/components/modal/Modal.test.tsx @@ -0,0 +1,37 @@ +// import { render, screen, fireEvent } from "@testing-library/react"; +// import Modal from "./Modal"; + +// test("Modal opens when button is clicked", () => { +// render(); + +// const openButton = screen.getByText("Open Modal"); +// fireEvent.click(openButton); + +// const modalTitle = screen.getByText("Modal Title"); +// expect(modalTitle).toBeInTheDocument(); +// }); + +// test("Modal closes when Close button is clicked", () => { +// render(); + +// const openButton = screen.getByText("Open Modal"); +// fireEvent.click(openButton); + +// const closeButton = screen.getByText("Close"); +// fireEvent.click(closeButton); + +// const modalTitle = screen.queryByText("Modal Title"); +// expect(modalTitle).not.toBeInTheDocument(); +// }); + +// test("Modal performs action when Action button is clicked", () => { +// render(); + +// const openButton = screen.getByText("Open Modal"); +// fireEvent.click(openButton); + +// const actionButton = screen.getByText("Action"); +// fireEvent.click(actionButton); + +// // Add your assertions for the action here +// }); \ No newline at end of file diff --git a/frontend/occupi-web/src/components/modal/Modal.tsx b/frontend/occupi-web/src/components/modal/Modal.tsx index 19d4757e..0e8491cb 100644 --- a/frontend/occupi-web/src/components/modal/Modal.tsx +++ b/frontend/occupi-web/src/components/modal/Modal.tsx @@ -16,22 +16,7 @@ import { ProfileComponent } from "@components/index"; -interface User { - id: string; - name: string; - role: string; - team: string; - status: string; - email: string; - bookings: number; - avatar: string; -} - -interface OccupancyModalProps { - user: User; -} - -export default function OccupancyModal({ user }: OccupancyModalProps) { +export default function OccupancyModal() { const { isOpen, onOpen, onOpenChange } = useDisclosure(); return ( @@ -50,19 +35,13 @@ export default function OccupancyModal({ user }: OccupancyModalProps) { {(onClose) => ( <> - {user.name}'s Office Occupancy Stats + User Office Occupancy Stats -
{/* User Profile and Key Stats Section */}
- +
diff --git a/frontend/occupi-web/src/components/occupancyRatingChart/OccupancyRating.tsx b/frontend/occupi-web/src/components/occupancyRatingChart/OccupancyRating.tsx index fb61b1c8..8eb9e077 100644 --- a/frontend/occupi-web/src/components/occupancyRatingChart/OccupancyRating.tsx +++ b/frontend/occupi-web/src/components/occupancyRatingChart/OccupancyRating.tsx @@ -1,43 +1,42 @@ import { - PieChart, - Pie, - Cell, - Tooltip, - Legend, - ResponsiveContainer, -} from "recharts"; - -const occupancyData = [ - { name: "In Office", value: 70 }, - { name: "Remote", value: 30 }, -]; - -const COLORS = ["#0088FE", "#00C49F"]; - -export default function OccupancyRatingChart() { - return ( - <> -

Occupancy Rating

- - - - {occupancyData.map((_entry, index) => ( - - ))} - - - - - - - ); -} \ No newline at end of file + PieChart, + Pie, + Cell, + Tooltip, + Legend, + ResponsiveContainer, + } from "recharts"; + + const occupancyData = [ + { name: "In Office", value: 70 }, + { name: "Remote", value: 30 }, + ]; + + const COLORS = ["#0088FE", "#00C49F"]; + + export default function OccupancyRatingChart() { + return ( +
+

Occupancy Rating

+ + + + {occupancyData.map((_entry, index) => ( + + ))} + + + + + +
+ ); + } \ No newline at end of file diff --git a/frontend/occupi-web/src/components/sideNavComponent/SideNav.tsx b/frontend/occupi-web/src/components/sideNavComponent/SideNav.tsx index 12097b0d..c0a210f3 100644 --- a/frontend/occupi-web/src/components/sideNavComponent/SideNav.tsx +++ b/frontend/occupi-web/src/components/sideNavComponent/SideNav.tsx @@ -143,14 +143,14 @@ const SideNav = () => { : }
-
+
{sidebarcontent.map((button_content, index) => { + const navigate = useNavigate(); const [isSearchVisible, setIsSearchVisible] = useState(false); + function navigateTo(path: string) { + navigate(path); + } + return (
@@ -72,10 +78,34 @@ const TopNav = (props: TopNavProps) => { > - + + + + + + navigateTo(key.toString())}> + +

Signed in as

+

@tinasheutstin

+
+ }>Notifications + }>Settings + }>Help/FAQ + }>Logout +
+
) } -export default TopNav; +export default TopNav; \ No newline at end of file diff --git a/frontend/occupi-web/src/login.png b/frontend/occupi-web/src/login.png new file mode 100644 index 00000000..261f040e Binary files /dev/null and b/frontend/occupi-web/src/login.png differ diff --git a/frontend/occupi-web/src/main.tsx b/frontend/occupi-web/src/main.tsx index 0903860f..af441da9 100644 --- a/frontend/occupi-web/src/main.tsx +++ b/frontend/occupi-web/src/main.tsx @@ -3,15 +3,11 @@ import ReactDOM from 'react-dom/client' import App from './App.tsx' import './index.css' import {NextUIProvider} from "@nextui-org/react"; -import { UserProvider } from 'UserContext'; ReactDOM.createRoot(document.getElementById('root')!).render( - - - - + , ) diff --git a/frontend/occupi-web/src/pages/Login/LoginForm.test.tsx b/frontend/occupi-web/src/pages/Login/LoginForm.test.tsx index 946d206f..dab24a13 100644 --- a/frontend/occupi-web/src/pages/Login/LoginForm.test.tsx +++ b/frontend/occupi-web/src/pages/Login/LoginForm.test.tsx @@ -1,106 +1,86 @@ -// import { expect, test, describe, mock } from "bun:test"; -// import { createRoot } from "react-dom/client"; -// import { BrowserRouter } from "react-router-dom"; -// import LoginForm from "./LoginForm"; -// import AuthService from "AuthService"; -// import { UserProvider } from "UserContext"; - -// describe("LoginForm", () => { -// test("renders without crashing", () => { -// document.body.innerHTML = '
'; -// const rootElement = document.getElementById("root"); -// if (!rootElement) throw new Error("Root element not found"); +/// +import { describe, test, expect, mock, beforeEach } from "bun:test"; +import { render, screen } from "@testing-library/react"; +import LoginForm from "./LoginForm"; // Adjust the import path as needed +import { ClassAttributes, InputHTMLAttributes } from "react"; +import { JSX } from "react/jsx-runtime"; + +// Mock the necessary dependencies +mock.module("react-router-dom", () => ({ + useNavigate: () => mock(() => {}), +})); + +mock.module("@assets/index", () => ({ + loginpng: "mock-login-image.png", + OccupiLogo: () =>
Occupi Logo
, +})); + +mock.module("@components/index", () => ({ + Checkbox: (props: JSX.IntrinsicAttributes & ClassAttributes & InputHTMLAttributes) => , + GradientButton: ({ Text, clickEvent, isLoading, isClickable }: { Text: string, clickEvent: () => void, isLoading: boolean, isClickable: boolean }) => ( + + ), + InputBox: ({ type, placeholder, label, submitValue }: { type: string, placeholder: string, label: string, submitValue: (value: string, flag: boolean) => void }) => ( + submitValue(e.target.value, true)} + /> + ), +})); + +mock.module("./WebAuthn", () => ({ + registerCredential: mock(() => Promise.resolve({ id: "mock-credential-id" })), + authenticateWithCredential: mock(() => Promise.resolve({ id: "mock-assertion-id" })), +})); + +describe("LoginForm", () => { + beforeEach(() => { + // Restore all mocks before each test + mock.restore(); + }); + + test("renders LoginForm correctly", () => { + render(); + expect(screen.getByText("Welcome back to Occupi.")).toBeDefined(); + expect(screen.getByText("Predict. Plan. Perfect")).toBeDefined(); + expect(screen.getByTestId("occupi-logo")).toBeDefined(); + }); + + test('renders input fields correctly', () => { + // render(); + const emailInputs = screen.getAllByPlaceholderText(/Enter your email address/); + const passwordInputs = screen.getAllByPlaceholderText(/Enter your password/); + expect(emailInputs).toHaveLength(1); // Ensure there's only one email input + expect(passwordInputs).toHaveLength(1); // Ensure there's only one password input + }); + + + //Intergration tests + // test("disables buttons when form is invalid", () => { + // // render(); + // const authButton = screen.getByText("Auth"); + // const registerButton = screen.getByText("Register"); -// const root = createRoot(rootElement); -// root.render( -// -// -// -// -// -// ); -// expect(document.body.innerHTML).toContain("Welcome back to Occupi."); -// }); - -// test("handles email input", () => { -// document.body.innerHTML = '
'; -// const rootElement = document.getElementById("root"); -// if (!rootElement) throw new Error("Root element not found"); + // expect(authButton).toHaveProperty("disabled", true); + // expect(registerButton).toHaveProperty("disabled", true); + // }); + + // test("enables buttons when form is valid", () => { + // // render(); + // const emailInput = screen.getByPlaceholderText("Enter your email address"); + // const passwordInput = screen.getByPlaceholderText("Enter your password"); -// const root = createRoot(rootElement); -// root.render( -// -// -// -// -// -// ); - -// const emailInput = document.querySelector('input[type="email"]') as HTMLInputElement | null; -// if (!emailInput) throw new Error('Email input not found'); - -// emailInput.value = "test@example.com"; -// emailInput.dispatchEvent(new Event("input")); - -// expect(emailInput.value).toBe("test@example.com"); -// }); - -// test("handles password input", () => { -// document.body.innerHTML = '
'; -// const rootElement = document.getElementById("root"); -// if (!rootElement) throw new Error("Root element not found"); + // fireEvent.change(emailInput, { target: { value: "test@example.com" } }); + // fireEvent.change(passwordInput, { target: { value: "password123" } }); -// const root = createRoot(rootElement); -// root.render( -// -// -// -// -// -// ); - -// const passwordInput = document.querySelector('input[type="password"]') as HTMLInputElement | null; -// if (!passwordInput) throw new Error('Password input not found'); - -// passwordInput.value = "password"; -// passwordInput.dispatchEvent(new Event("input")); - -// expect(passwordInput.value).toBe("password"); -// }); - -// test("login functionality", async () => { -// const mockLogin = mock(() => Promise.resolve({ message: "Login successful" })); -// AuthService.login = mockLogin; - -// document.body.innerHTML = '
'; -// const rootElement = document.getElementById("root"); -// if (!rootElement) throw new Error("Root element not found"); - -// const root = createRoot(rootElement); -// root.render( -// -// -// -// -// -// ); - -// const emailInput = document.querySelector('input[type="email"]') as HTMLInputElement | null; -// const passwordInput = document.querySelector('input[type="password"]') as HTMLInputElement | null; -// const loginButton = document.querySelector('button') as HTMLButtonElement | null; - -// if (!emailInput || !passwordInput || !loginButton) throw new Error('Form elements not found'); - -// emailInput.value = "test@example.com"; -// emailInput.dispatchEvent(new Event("input")); - -// passwordInput.value = "password"; -// passwordInput.dispatchEvent(new Event("input")); - -// loginButton.click(); - -// await new Promise(resolve => setTimeout(resolve, 0)); - -// expect(mockLogin).toHaveBeenCalledWith("test@example.com", "password"); -// }); -// }); + // const authButton = screen.getByText("Auth"); + // const registerButton = screen.getByText("Register"); + + // expect(authButton).toHaveProperty("disabled", false); + // expect(registerButton).toHaveProperty("disabled", false); + // }); +}); diff --git a/frontend/occupi-web/src/pages/Login/LoginForm.tsx b/frontend/occupi-web/src/pages/Login/LoginForm.tsx index 6a2d126a..ad2f87ca 100644 --- a/frontend/occupi-web/src/pages/Login/LoginForm.tsx +++ b/frontend/occupi-web/src/pages/Login/LoginForm.tsx @@ -8,7 +8,6 @@ import { useUser } from "UserContext"; const LoginForm = (): JSX.Element => { const navigate = useNavigate(); - const { setUserDetails } = useUser(); const [form, setForm] = useState<{ email: string, diff --git a/frontend/occupi-web/src/pages/Login/style.css b/frontend/occupi-web/src/pages/Login/style.css new file mode 100644 index 00000000..90c61b98 --- /dev/null +++ b/frontend/occupi-web/src/pages/Login/style.css @@ -0,0 +1,120 @@ +.box .image-container{ + @apply h-[400] w-[400]; +} +.box .image{ + @apply h-[200] w-[200] absolute object-cover right-[800px] top-[197px]; + /* left: 156px; */ +} +.box { + @apply h-[846px] relative w-[430px] ml-[900px] +} +.box .group { + @apply h-[846px] w-[436px] left-0 top-0; + /* position: fixed; */ +} +.box .overlap { + @apply h-[400px] absolute w-[434px] left-0 top-0; +} +.box .frame { + @apply h-[295px] absolute w-[393px] left-[17px] top-0; +} +.box .div { + @apply h-[181px] absolute w-[434px] left-0 top-[219px]; +} +.box .text-wrapper { + @apply text-[rgba(10,10,10,1)] text-[50px] not-italic font-semibold h-[152px] tracking-[-0.5px] leading-[50px] absolute w-[430px] text-left left-0 top-10; + font-family: "Inter", Helvetica; +} +.box .text-wrapper-2 { + @apply text-[color:var(--colours-black-950)] text-[length:var(--inter-extra-light-24-font-size)] font-[number:var(--inter-extra-light-24-font-weight)] h-[29px] tracking-[var(--inter-extra-light-24-letter-spacing)] leading-[var(--inter-extra-light-24-line-height)] absolute w-[430px] text-left left-0 top-[155px]; + font-family: var(--inter-extra-light-24-font-family); + font-style: var(--inter-extra-light-24-font-style); +} +.box .group-2 { + @apply h-[73px] absolute w-[434px] left-0 top-[450px]; +} +.box .overlap-group { + @apply bg-[rgba(235,235,235,1)] h-[50px] absolute w-[430px] rounded-[15px] left-0 top-[23px]; +} +.box .text-wrapper-3 { + @apply text-[rgba(10,10,10,1)] text-xs not-italic font-[number:var(--inter-extra-light-12-font-weight)] h-[18px] tracking-[var(--inter-extra-light-12-letter-spacing)] leading-[var(--inter-extra-light-12-line-height)] absolute w-[391px] left-[18px] top-4; + font-family: "Inter", Helvetica; +} +.box .text-wrapper-4 { + @apply text-[color:var(--colours-black-950)] text-[length:var(--inter-regular-16-font-size)] font-[number:var(--inter-regular-16-font-weight)] h-[18px] tracking-[var(--inter-regular-16-letter-spacing)] leading-[var(--inter-regular-16-line-height)] absolute w-[430px] left-0 top-0; + font-family: var(--inter-regular-16-font-family); + font-style: var(--inter-regular-16-font-style); +} +.box .group-3 { + @apply h-[73px] absolute w-[436px] left-0 top-[553px]; +} +.box .div2 { + @apply text-[color:var(--colours-black-950)] text-[length:var(--inter-regular-16-font-size)] font-[number:var(--inter-regular-16-font-weight)] h-[18px] tracking-[var(--inter-regular-16-letter-spacing)] leading-[var(--inter-regular-16-line-height)] absolute w-[430px] left-0 top-0; + font-family: var(--inter-regular-16-font-family); + font-style: var(--inter-regular-16-font-style); +} +.box .overlap-2 { + @apply bg-[color:var(--colours-white-800)] border-[color:var(--colours-red-650)] h-[50px] absolute w-[430px] rounded-[15px] border-2 border-solid left-0 top-[23px]; +} +.box .text-wrapper-5 { + @apply text-[color:var(--colours-black-950)] text-[length:var(--inter-extra-light-12-font-size)] font-[number:var(--inter-extra-light-12-font-weight)] h-[18px] tracking-[var(--inter-extra-light-12-letter-spacing)] leading-[var(--inter-extra-light-12-line-height)] absolute w-[325px] left-[13px] top-3.5; + font-family: var(--inter-extra-light-12-font-family); + font-style: var(--inter-extra-light-12-font-style); +} +.box .eye-closed { + @apply h-6 absolute w-6 left-[385px] top-[11px]; +} +.box .overlap-3 { + @apply h-[18px] absolute w-[430px] left-0 top-0; +} +.box .text-wrapper-6 { + @apply text-[color:var(--colours-red-650)] text-[length:var(--inter-regular-16-font-size)] font-[number:var(--inter-regular-16-font-weight)] h-[18px] tracking-[var(--inter-regular-16-letter-spacing)] leading-[var(--inter-regular-16-line-height)] absolute text-right w-[430px] left-0 top-0; + font-family: var(--inter-regular-16-font-family); + font-style: var(--inter-regular-16-font-style); +} +.box .overlap-4 { + @apply h-[29px] absolute w-[430px] left-0 top-[659px]; +} +.box .group-4 { + @apply h-[29px] absolute w-[425px] left-0 top-0; +} +.box .text-wrapper-7 { + @apply text-[color:var(--colours-green-600)] text-[15] font-[number:var(--inter-semi-bold-12-font-weight)] h-[29px] tracking-[var(--inter-semi-bold-12-letter-spacing)] leading-[var(--inter-semi-bold-12-line-height)] absolute w-[394px] left-[29px] top-[15px]; + font-family: var(--inter-semi-bold-12-font-family); + font-style: var(--inter-semi-bold-12-font-style); +} +.box .check-square { + @apply h-6 absolute w-[29px] left-0 top-[3px]; +} +.box .text-wrapper-8 { + @apply text-[color:var(--colours-green-600)] text-[15] font-[number:var(--inter-semi-bold-12-font-weight)] h-[29px] tracking-[var(--inter-semi-bold-12-letter-spacing)] leading-[var(--inter-semi-bold-12-line-height)] absolute text-right w-[423px] left-[7px] top-[15px]; + font-family: var(--inter-semi-bold-12-font-family); + font-style: var(--inter-semi-bold-12-font-style); +} +.box .login-button { + @apply bg-white shadow-[var(--shadow-9-7-30-10)] box-border h-[55px] opacity-90 overflow-hidden relative w-[450px] rounded-[18px] left-0 top-[729px]; + all: unset; + background: linear-gradient(90deg, rgb(96.72, 77.4, 99.75) 1.53%, rgb(133.72, 234.6, 204.34) 31.5%, rgb(178.21, 252.03, 57.55) 66.99%, rgb(238.07, 240.12, 96.25) 97%); + /* so i can make it more bright or opaque */ +} +.box .text-wrapper-9 { + @apply text-[color:var(--colours-white-1000)] text-[27px] font-[number:var(--inter-semi-bold-16-font-weight)] h-[29px] tracking-[var(--inter-semi-bold-16-letter-spacing)] leading-[var(--inter-semi-bold-16-line-height)] absolute text-center w-[350px] left-[39px] top-[22px]; + font-family: var(--inter-semi-bold-16-font-family); + font-style: var(--inter-semi-bold-16-font-style); +} +.box .overlap-5 { + @apply h-[29px] absolute w-[430px] left-0 top-[817px]; +} +.box .p { + @apply text-[color:var(--colours-black-950)] text-[15] font-[number:var(--inter-regular-12-font-weight)] h-[29px] tracking-[var(--inter-regular-12-letter-spacing)] leading-[var(--inter-regular-12-line-height)] absolute text-center w-[430px] left-0 top-0; + font-family: var(--inter-regular-12-font-family); + font-style: var(--inter-regular-12-font-style); +} +.box .text-wrapper-10 { + @apply text-[color:var(--colours-green-600)] text-[15] font-[number:var(--inter-regular-12-font-weight)] h-[29px] tracking-[var(--inter-regular-12-letter-spacing)] leading-[var(--inter-regular-12-line-height)] absolute w-[203px] left-[210px] top-[15px]; + font-family: var(--inter-regular-12-font-family); + font-style: var(--inter-regular-12-font-style); +} + + + \ No newline at end of file diff --git a/frontend/occupi-web/src/pages/index.ts b/frontend/occupi-web/src/pages/index.ts index ea1ac789..52d85f7e 100644 --- a/frontend/occupi-web/src/pages/index.ts +++ b/frontend/occupi-web/src/pages/index.ts @@ -1,24 +1,15 @@ -import LoginForm from "./Login/LoginForm"; -import OtpPage from "./otp-page/OtpPage"; -import LandingPage from "./landing-page/LandingPage"; -import Settings from "./settings-page/Settings"; +import LoginForm from './Login/LoginForm' +import OtpPage from './otp-page/OtpPage' +import LandingPage from './landing-page/LandingPage' +import Settings from './settings-page/Settings' import Dashboard from "./Dashboard/Dashboard"; -import Appearance from "./appearance-page/Appearance"; -// import OverView from '../components/bookings/Bookings'; -import Analysis from "./analysis/Analysis"; -import PDFReport from "./reports/PDFReport"; -import Visitation from "./visitations/Visitations"; -import Faq from "./faq/Faq"; +import Appearance from './appearance-page/Appearance'; +import OverView from '../components/bookings/Bookings'; +import Analysis from './analysis/Analysis'; +import PDFReport from './reports/PDFReport'; +import Visitation from './visitations/Visitations'; +import Faq from './faq/Faq'; export { - LoginForm, - OtpPage, - LandingPage, - Settings, - Dashboard, - Appearance, -// OverView, - Analysis, - PDFReport, - Visitation, - Faq, -}; + LoginForm, OtpPage, LandingPage, Settings, + Dashboard,Appearance, OverView, Analysis, PDFReport, Visitation, Faq +} \ No newline at end of file diff --git a/frontend/occupi-web/src/pages/landing-page/LandingPage.css b/frontend/occupi-web/src/pages/landing-page/LandingPage.css new file mode 100644 index 00000000..e69de29b diff --git a/frontend/occupi-web/src/pages/otp-page/OtpPage.css b/frontend/occupi-web/src/pages/otp-page/OtpPage.css new file mode 100644 index 00000000..9a853e3a --- /dev/null +++ b/frontend/occupi-web/src/pages/otp-page/OtpPage.css @@ -0,0 +1,39 @@ +.box .image-container { + @apply h-[100] w-[100]; +} +.box .image { + @apply h-[100] w-[100] absolute object-cover right-[650px] top-[197px]; + /* left: 156px; */ +} +.box { + @apply h-[846px] relative w-[430px] ml-[900px]; +} +.box .group { + @apply h-[846px] w-[436px] left-0 top-0; + /* position: fixed; */ +} +.box .overlap { + @apply h-[400px] absolute w-[434px] left-0 top-0; +} +.box .frame { + @apply h-[295px] absolute w-[393px] left-[17px] top-0; +} +.box .div { + @apply h-[181px] absolute w-[434px] left-0 top-[219px]; +} +.box .text-wrapper { + @apply text-[rgba(10,10,10,1)] text-[50px] not-italic font-semibold h-[152px] left-[-100px] tracking-[-0.5px] leading-[50px] absolute w-[500px] text-left top-10; + font-family: "Inter", Helvetica; +} +.box .text-wrapper-2 { + @apply text-[color:var(--colours-gray-1000)] text-[length:var(--inter-extra-light-24-font-size)] font-[number:var(--inter-extra-light-24-font-weight)] h-[29px] left-[-100px] tracking-[var(--inter-extra-light-24-letter-spacing)] leading-[var(--inter-extra-light-24-line-height)] absolute w-[430px] text-left top-[155px]; + font-family: var(--inter-extra-light-24-font-family); + font-style: var(--inter-extra-light-24-font-style); +} +.box .group-2 { + @apply h-[73px] absolute w-[434px] left-0 top-[490px]; +} +.button-container { + @apply flex justify-end mt-0; + /* Adjust the spacing as needed */ +} diff --git a/frontend/occupi-web/src/pages/otp-page/OtpPage.test.tsx b/frontend/occupi-web/src/pages/otp-page/OtpPage.test.tsx index a8fd9a50..44626196 100644 --- a/frontend/occupi-web/src/pages/otp-page/OtpPage.test.tsx +++ b/frontend/occupi-web/src/pages/otp-page/OtpPage.test.tsx @@ -1,4 +1,5 @@ import { expect, mock, test } from "bun:test"; +import { render, screen } from "@testing-library/react"; import OtpPage from "./OtpPage"; // Adjust the import path as needed import { createElement } from "react"; // import '@testing-library/jest-dom'; @@ -7,13 +8,13 @@ import { createElement } from "react"; interface OtpComponentProps { setOtp: (otp: string, isValid: boolean) => void; } -// test("OtpPage renders correctly", () => { -// render(); +test("OtpPage renders correctly", () => { + render(); -// expect(screen.getByText("We sent you an email with a code")).toBeDefined(); -// expect(screen.getByText("Please enter it to continue")).toBeDefined(); -// expect(screen.getByText("Complete")).toBeDefined(); -// }); + expect(screen.getByText("We sent you an email with a code")).toBeDefined(); + expect(screen.getByText("Please enter it to continue")).toBeDefined(); + expect(screen.getByText("Complete")).toBeDefined(); +}); diff --git a/frontend/occupi-web/src/pages/otp-page/OtpPage.tsx b/frontend/occupi-web/src/pages/otp-page/OtpPage.tsx index d9e9caa9..f79050e8 100644 --- a/frontend/occupi-web/src/pages/otp-page/OtpPage.tsx +++ b/frontend/occupi-web/src/pages/otp-page/OtpPage.tsx @@ -1,85 +1,50 @@ -// OtpPage.tsx -import { useState, useEffect } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; -import { OccupiLogo, login_image } from "@assets/index"; -import { GradientButton, OtpComponent } from "@components/index"; -import AuthService from "AuthService"; -import { useUser } from "UserContext"; +import { useState } from "react"; +import { + //loginpng, + OccupiLogo, login_image} from "@assets/index"; +import { GradientButton, OtpComponent } from "@components/index"; const OtpPage = () => { - const location = useLocation(); - const navigate = useNavigate(); - const { setUserDetails } = useUser(); - const [email, setEmail] = useState(""); - const [otp, setOTP] = useState<{ otp: string, validity: boolean }>({ otp: "", validity: false }); + const [otp, setOTP] = useState<{ + otp: string, + validity: boolean + }>({otp: "", validity: false}); const [isloading, setIsLoading] = useState(false); - const [error, setError] = useState(""); - - useEffect(() => { - const state = location.state as { email?: string } | null; - if (state && state.email) { - setEmail(state.email); - } else { - setError("Email not provided. Please start the login process again."); - // Optionally, redirect to login page after a short delay - // setTimeout(() => navigate('/login'), 3000); - } - }, [location.state, navigate]); - - async function verifyOTP() { - if (!email) { - setError("Email not available. Please start the login process again."); - return; - } + function SendOTP() { setIsLoading(true); - setError(""); - try { - const response = await AuthService.verifyOtpLogin(email, otp.otp.replace(/,/g, "")); - console.log("OTP verification response:", response); - - // Uncomment these lines if you want to fetch and set user details - const userDetails = await AuthService.getUserDetails(email); - setUserDetails(userDetails); - - navigate("/dashboard/overview"); - } catch (error) { - console.error("OTP verification error:", error); - setError("OTP verification failed. Please try again."); - } finally { + // login functionality should happen here, use axios if possible + setTimeout(() => { setIsLoading(false); - } + }, 2000); } return ( -
-
-
- welcomes +
+
+
+ welcomes
-
-
- -
-

We sent you an email with a code

-

- {email ? `Please enter it to continue (${email})` : "Please enter it to continue"} -

- - { - setOTP({ ...otp, otp: otp_val.join(""), validity: validity }); - }} /> - -
- +
+
+ +
+

We sent you an email with a code

+

Please enter it to continue

+ + { + setOTP({ ... otp, otp : otp_val.toString()}) + setOTP({ ... otp, validity : validity}) + }}/> + +
+ +
- - {error &&

{error}

} -
); }; -export default OtpPage; \ No newline at end of file +export default OtpPage; diff --git a/frontend/occupi-web/src/pages/visitations/Visitations.tsx b/frontend/occupi-web/src/pages/visitations/Visitations.tsx index af01afef..7c698b29 100644 --- a/frontend/occupi-web/src/pages/visitations/Visitations.tsx +++ b/frontend/occupi-web/src/pages/visitations/Visitations.tsx @@ -1,245 +1,8 @@ -// import React, { useState, useEffect } from 'react'; -// import axios from 'axios'; - -// interface ApiUser { -// _id: string; -// occupiId: string; -// details?: { -// name: string; -// contactNo?: string; -// }; -// email: string; -// role: string; -// departmentNo?: string; -// status?: string; -// onSite: boolean; -// position?: string; -// } - -// interface FormattedUser extends ApiUser { -// bookings: number; -// } - -// interface ApiResponse { -// data: ApiUser[]; -// meta: { -// currentPage: number; -// totalPages: number; -// totalResults: number; -// }; -// } - -// const UserTable: React.FC = () => { -// const [users, setUsers] = useState([]); -// const [loading, setLoading] = useState(true); -// const [error, setError] = useState(null); -// const [totalPages, setTotalPages] = useState(0); - -// useEffect(() => { -// fetchUsers(); -// }, []); - -// const fetchUsers = async () => { -// try { -// setLoading(true); -// const response = await axios.get('/api/filter-users'); - -// const fetchedUsers = response.data.data.map(user => ({ -// ...user, -// bookings: Math.floor(Math.random() * 5), // Random number for bookings as it's not in the API -// })); - -// setUsers(fetchedUsers); -// setTotalPages(response.data.meta.totalPages); -// setLoading(false); -// } catch (err) { -// if (axios.isAxiosError(err)) { -// setError(`Failed to fetch users: ${err.response?.data?.message || err.message}`); -// } else { -// setError('An unknown error occurred'); -// } -// setLoading(false); -// } -// }; - -// if (loading) return
Loading...
; -// if (error) return
Error: {error}
; - -// return ( -//
-// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// {users.map((user) => ( -// -// -// -// -// -// -// -// -// -// -// -// ))} -// -//
OCCUPI-IDNAMEROLEDEPARTMENTEMAILSTATUSPOSITIONCONTACT NOBOOKINGS THIS WEEK
{user.occupiId}{user.details?.name || 'N/A'}{user.role}{user.departmentNo || 'N/A'}{user.email}{user.onSite ? 'ONSITE' : (user.status || 'OFFSITE')}{user.position || 'N/A'}{user.details?.contactNo || 'N/A'}{user.bookings}
-//
Total Pages: {totalPages}
-//
-// ); -// }; - -//import React from 'react'; -import React, { useState } from 'react'; -import { NextUIProvider, Card, CardBody, CardHeader } from "@nextui-org/react"; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'; -import { FaChartLine, FaChartPie, FaUsers, FaChartBar } from 'react-icons/fa'; -import { DragDropContext, Droppable, Draggable, DroppableProvided, DraggableProvided, DropResult } from 'react-beautiful-dnd'; - -const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042']; - -const Visitations: React.FC = () => { - const weeklyOccupancyData = [ - { day: 'Mon', occupancy: 65 }, - { day: 'Tue', occupancy: 75 }, - { day: 'Wed', occupancy: 85 }, - { day: 'Thu', occupancy: 80 }, - { day: 'Fri', occupancy: 70 }, - ]; - - const departmentOccupancyData = [ - { name: 'Engineering', value: 40 }, - { name: 'Marketing', value: 25 }, - { name: 'Sales', value: 20 }, - { name: 'HR', value: 15 }, - ]; - - const averageOccupancy = weeklyOccupancyData.reduce((sum, day) => sum + day.occupancy, 0) / weeklyOccupancyData.length; - const peakOccupancy = Math.max(...weeklyOccupancyData.map(day => day.occupancy)); - - const [graphOrder, setGraphOrder] = useState(['weekly', 'department']); - - const onDragEnd = (result: DropResult) => { - if (!result.destination) return; - const items = Array.from(graphOrder); - const [reorderedItem] = items.splice(result.source.index, 1); - items.splice(result.destination.index, 0, reorderedItem); - setGraphOrder(items); - }; - - const renderGraph = (graph: string) => { - if (graph === 'weekly') { - return ( - - - - - - - - - ); - } else { - return ( - - `${name} ${(percent * 100).toFixed(0)}%`} - > - {departmentOccupancyData.map((_entry, index) => ( - - ))} - - - - ); - } - }; +const Visitations = () => { return ( - -
-

Office Occupancy Dashboard

- - {/* Card Section */} -
- - -
- -

Average Occupancy

-
-

{averageOccupancy.toFixed(1)}%

-
-
- - -
- -

Peak Occupancy

-
-

{peakOccupancy}%

-
-
-
- - {/* Graph Section */} - - - {(provided: DroppableProvided) => ( -
- {graphOrder.map((graph, index) => ( - - {(provided: DraggableProvided) => ( -
- - -

- {graph === 'weekly' ? 'Weekly Occupancy Trend' : 'Department Occupancy Distribution'} -

- {graph === 'weekly' ? : } -
- - - {renderGraph(graph)} - - -
-
- )} -
- ))} - {provided.placeholder} -
- )} -
-
-
-
- ); -}; +
Visitations
+ ) +} -export default Visitations; \ No newline at end of file +export default Visitations \ No newline at end of file diff --git a/frontend/occupi-web/vite.config.ts b/frontend/occupi-web/vite.config.ts index 7a9a370e..95d79fe0 100644 --- a/frontend/occupi-web/vite.config.ts +++ b/frontend/occupi-web/vite.config.ts @@ -2,8 +2,8 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import path from "path"; import tsconfigPaths from "vite-tsconfig-paths"; -import mkcert from "vite-plugin-mkcert"; import fs from 'fs'; +import mkcert from 'vite-plugin-mkcert'; // Define paths to the certificates let keyPath = ''; @@ -17,10 +17,12 @@ if (process.env.NODE_ENV === 'preview-dev') { certPath = '/etc/letsencrypt/live/app.occupi.tech/fullchain.pem'; } +// https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), tsconfigPaths(), mkcert({ hosts: ['localhost', 'dev.occupi.tech', 'app.occupi.tech'], })], + resolve: { alias: { @@ -28,32 +30,11 @@ export default defineConfig({ }, }, + //Added this to fix non-polling, gosh it was the ghetto there by manual refesh server: { - // https: true, - proxy: { - '/api': { - target: 'https://dev.occupi.tech', - changeOrigin: true, - secure: false, - rewrite: (path) => path.replace(/^\/api/, '/api'), - configure: (proxy) => { - proxy.on('error', (err) => { - console.log('proxy error', err); - }); - proxy.on('proxyReq', (proxyReq, req) => { - console.log('Sending Request to the Target:', req.method, req.url); - }); - proxy.on('proxyRes', (proxyRes, req) => { - console.log('Received Response from the Target:', proxyRes.statusCode, req.url); - }); - }, - }, - '/auth': { - target: 'https://dev.occupi.tech', - changeOrigin: true, - secure: false, - rewrite: (path) => path.replace(/^\/auth/, '/auth'), - } + watch: { + usePolling: true, + interval: 1000, // Adjust the interval if needed }, https: process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'preview' || process.env.NODE_ENV === 'development' ? {} : { key: fs.readFileSync(keyPath), @@ -62,4 +43,4 @@ export default defineConfig({ host: '0.0.0.0', port: 4173, }, -}); \ No newline at end of file +}); diff --git a/frontend/occupi/app/favicon.ico b/frontend/occupi/app/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/frontend/occupi/app/favicon.ico differ diff --git a/frontend/occupi/app/globals.css b/frontend/occupi/app/globals.css new file mode 100644 index 00000000..875c01e8 --- /dev/null +++ b/frontend/occupi/app/globals.css @@ -0,0 +1,33 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} diff --git a/frontend/occupi/app/layout.tsx b/frontend/occupi/app/layout.tsx new file mode 100644 index 00000000..3314e478 --- /dev/null +++ b/frontend/occupi/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/frontend/occupi/app/page.tsx b/frontend/occupi/app/page.tsx new file mode 100644 index 00000000..5705d4ea --- /dev/null +++ b/frontend/occupi/app/page.tsx @@ -0,0 +1,113 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+
+

+ Get started by editing  + app/page.tsx +

+ +
+ +
+ Next.js Logo +
+ +
+
+ ); +} diff --git a/frontend/occupi/next.config.mjs b/frontend/occupi/next.config.mjs new file mode 100644 index 00000000..1f532917 --- /dev/null +++ b/frontend/occupi/next.config.mjs @@ -0,0 +1,18 @@ +/** @type {import('next').NextConfig} */ + +const isDev = process.env.NODE_ENV === 'development'; +const isBackend = process.env.APP_ENV === 'backend'; + +const prodConfig = { + basePath: '/occupi', + assetPrefix: '/occupi/', + output: 'export', +}; + +const backendConfig = { + basePath: '/documentation', + assetPrefix: '/documentation/', + output: 'export', +} + +export default isDev ? {} : isBackend ? backendConfig : prodConfig; diff --git a/frontend/occupi/postcss.config.mjs b/frontend/occupi/postcss.config.mjs new file mode 100644 index 00000000..1a69fd2a --- /dev/null +++ b/frontend/occupi/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/frontend/occupi/public/images/hero.svg b/frontend/occupi/public/images/hero.svg new file mode 100644 index 00000000..75357e77 --- /dev/null +++ b/frontend/occupi/public/images/hero.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/occupi/public/next.svg b/frontend/occupi/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/frontend/occupi/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/occupi/public/vercel.svg b/frontend/occupi/public/vercel.svg new file mode 100644 index 00000000..d2f84222 --- /dev/null +++ b/frontend/occupi/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/occupi/tailwind.config.ts b/frontend/occupi/tailwind.config.ts new file mode 100644 index 00000000..7e4bd91a --- /dev/null +++ b/frontend/occupi/tailwind.config.ts @@ -0,0 +1,20 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + backgroundImage: { + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + }, + }, + }, + plugins: [], +}; +export default config; diff --git a/frontend/occupi/tsconfig.json b/frontend/occupi/tsconfig.json new file mode 100644 index 00000000..e7ff90fd --- /dev/null +++ b/frontend/occupi/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/images/IMG_9675_1.jpg b/images/IMG_9675_1.jpg new file mode 100644 index 00000000..3eea1558 Binary files /dev/null and b/images/IMG_9675_1.jpg differ diff --git a/images/IMG_9675_12.jpg b/images/IMG_9675_12.jpg new file mode 100644 index 00000000..3eea1558 Binary files /dev/null and b/images/IMG_9675_12.jpg differ diff --git a/images/Kamo.jpg b/images/Kamo.jpg new file mode 100644 index 00000000..cb415f87 Binary files /dev/null and b/images/Kamo.jpg differ diff --git a/images/WhatsApp Image 2024-06-03 at 13.12.54_a03fd102.jpg b/images/WhatsApp Image 2024-06-03 at 13.12.54_a03fd102.jpg new file mode 100644 index 00000000..655ca0b5 Binary files /dev/null and b/images/WhatsApp Image 2024-06-03 at 13.12.54_a03fd102.jpg differ diff --git a/images/img.prof.jpg b/images/img.prof.jpg new file mode 100644 index 00000000..bee27d58 Binary files /dev/null and b/images/img.prof.jpg differ diff --git a/maestro/Binary_ANN.py b/maestro/Binary_ANN.py new file mode 100644 index 00000000..4da1f52b --- /dev/null +++ b/maestro/Binary_ANN.py @@ -0,0 +1,106 @@ +import pandas as pd +import matplotlib.pyplot as plt +from sklearn.model_selection import train_test_split +from sklearn.preprocessing import LabelEncoder, StandardScaler +from tensorflow.keras.models import Sequential +from tensorflow.keras.layers import Dense + +# Step 1: Read Data from CSV +df = pd.read_csv('datasets/Attendance_Prediction.csv') + +# Display the first few rows of the dataframe +print(df.head()) + +# Step 2: Visualize the Most Preferred Days +# Aggregate the data by Day_of_Week +visit_counts = df.groupby('Day_of_Week')['WillVisit'].sum().reset_index() + +# Sort the days of the week for better visualization +days_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] +visit_counts['Day_of_Week'] = pd.Categorical(visit_counts['Day_of_Week'], categories=days_order, ordered=True) +visit_counts = visit_counts.sort_values('Day_of_Week') + +# Plotting the bar chart +plt.figure(figsize=(10, 6)) +plt.bar(visit_counts['Day_of_Week'], visit_counts['WillVisit'], color='skyblue') +plt.xlabel('Day of the Week') +plt.ylabel('Number of Visits') +plt.title('Number of Visits per Day of the Week') +plt.xticks(rotation=45) +plt.grid(axis='y', linestyle='--', alpha=0.7) +plt.show() + +# Step 3: Preprocess Data for ANN +# Encode categorical features +label_encoders = {} +categorical_features = ['Day_of_Week'] + +for feature in categorical_features: + label_encoders[feature] = LabelEncoder() + df[feature] = label_encoders[feature].fit_transform(df[feature]) + +# Convert boolean features to integers +df['IsPreferredDay'] = df['IsPreferredDay'].astype(int) +df['IsEventDay'] = df['IsEventDay'].astype(int) +df['RemoteEmployee'] = df['RemoteEmployee'].astype(int) + +# Define features (X) and target (y) +X = df.drop(columns=['WillVisit', 'Date']) +y = df['WillVisit'] + +# Scale the features +scaler = StandardScaler() +X_scaled = scaler.fit_transform(X) + +# Split the data into training and testing sets +X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2, random_state=42) + +# Step 4: Train the ANN +# Build the ANN model +model = Sequential() +model.add(Dense(16, input_dim=X_train.shape[1], activation='relu')) +model.add(Dense(8, activation='relu')) +model.add(Dense(1, activation='sigmoid')) + +# Compile the model +model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) + +# Train the model +model.fit(X_train, y_train, epochs=50, batch_size=10, verbose=2) + +# Evaluate the model +loss, accuracy = model.evaluate(X_test, y_test) +print(f'Test Accuracy: {accuracy:.2f}') + +model.export('C:/Users/retha/Capstone/occupi/models/person_prediction_model/1') + +# Step 5: Predict on New Data +# Example new data +new_data = { + 'Day_of_Week': ['Tuesday', 'Tuesday'], + 'booked': [1, 0], + 'IsPreferredDay': [True, True], + 'IsEventDay': [False, False], + 'RemoteEmployee': [False, True] +} + +new_df = pd.DataFrame(new_data) + +# Encode the new data +for feature in categorical_features: + new_df[feature] = label_encoders[feature].transform(new_df[feature]) + +# Convert boolean features to integers +new_df['IsPreferredDay'] = new_df['IsPreferredDay'].astype(int) +new_df['IsEventDay'] = new_df['IsEventDay'].astype(int) +new_df['RemoteEmployee'] = new_df['RemoteEmployee'].astype(int) + +# Scale the new data +new_X_scaled = scaler.transform(new_df) + +# Predict using the trained model +predictions = model.predict(new_X_scaled) +predictions = (predictions > 0.5).astype(int) + +new_df['WillVisit'] = predictions +print(new_df) diff --git a/maestro/LSTM(1).py b/maestro/LSTM(1).py index cec6b0b5..7403eafc 100644 --- a/maestro/LSTM(1).py +++ b/maestro/LSTM(1).py @@ -10,7 +10,6 @@ from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import EarlyStopping from datetime import datetime -import joblib # Load the dataset file_path = 'datasets/Attendance_data(1).csv' @@ -75,7 +74,6 @@ scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) -joblib.dump(scaler, 'attendance_scaler.pkl') # Original Distribution of Number Attended plt.figure(figsize=(14, 6)) diff --git a/maestro/XGClassifier_monthly_ai.py b/maestro/XGClassifier_monthly_ai.py index 69533a7b..cfb01f20 100644 --- a/maestro/XGClassifier_monthly_ai.py +++ b/maestro/XGClassifier_monthly_ai.py @@ -9,7 +9,7 @@ import holidays # Load the CSV file -file_path = 'datasets/Monthly_OfficeCapacity (1).csv' +file_path = '../datasets/Monthly_OfficeCapacity (1).csv' df = pd.read_csv(file_path) # Data Preprocessing diff --git a/maestro/XGRegessor_monthly_ai.py b/maestro/XGRegessor_monthly_ai.py index f9409915..13667c8c 100644 --- a/maestro/XGRegessor_monthly_ai.py +++ b/maestro/XGRegessor_monthly_ai.py @@ -9,7 +9,7 @@ import holidays # Load the CSV file -file_path = 'datasets/Monthly_OfficeCapacity (1).csv' +file_path = '../datasets/Monthly_OfficeCapacity (1).csv' df = pd.read_csv(file_path) # Data Preprocessing diff --git a/models/person_prediction_model/1/fingerprint.pb b/models/person_prediction_model/1/fingerprint.pb new file mode 100644 index 00000000..b7dfcaca --- /dev/null +++ b/models/person_prediction_model/1/fingerprint.pb @@ -0,0 +1 @@ +Αε΅„Ε₯ιΗΏι‘Γ믍¨λΊο‰“ΟξΕΏ* άͺύη­‘©(θ·ΘςΣ’γσ_2 \ No newline at end of file diff --git a/models/person_prediction_model/1/saved_model.pb b/models/person_prediction_model/1/saved_model.pb new file mode 100644 index 00000000..a9020cf1 Binary files /dev/null and b/models/person_prediction_model/1/saved_model.pb differ diff --git a/models/person_prediction_model/1/variables/variables.data-00000-of-00001 b/models/person_prediction_model/1/variables/variables.data-00000-of-00001 new file mode 100644 index 00000000..8081fab6 Binary files /dev/null and b/models/person_prediction_model/1/variables/variables.data-00000-of-00001 differ diff --git a/models/person_prediction_model/1/variables/variables.index b/models/person_prediction_model/1/variables/variables.index new file mode 100644 index 00000000..e746a85e Binary files /dev/null and b/models/person_prediction_model/1/variables/variables.index differ diff --git a/occupi-backend/Dockerfile b/occupi-backend/Dockerfile new file mode 100644 index 00000000..3eca40d2 --- /dev/null +++ b/occupi-backend/Dockerfile @@ -0,0 +1,21 @@ +# Use the official Golang image as a base +FROM golang:1.22 + +# Set the current working directory inside the container +WORKDIR /app + +# pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change +COPY go.mod go.sum ./ +RUN go mod download && go mod verify + +# Copy the source code into the container +COPY . . + +# Build the Go application +RUN go build -o occupi-backend ./cmd/occupi-backend + +# Expose the port the app runs on +EXPOSE 8080 + +# Command to run the executable +CMD ["./occupi-backend"] \ No newline at end of file diff --git a/occupi-backend/configs/setup.go b/occupi-backend/configs/setup.go index 5a08f4a6..f7a50dc9 100644 --- a/occupi-backend/configs/setup.go +++ b/occupi-backend/configs/setup.go @@ -47,7 +47,6 @@ func ConnectToDatabase(args ...string) *mongo.Client { // Connect to MongoDB client, err := mongo.Connect(ctx, clientOptions) if err != nil { - fmt.Printf("Failed to connect to MongoDB: %s\n", err) logrus.Fatal(err) errv := client.Disconnect(ctx) logrus.Fatal(errv) @@ -56,13 +55,11 @@ func ConnectToDatabase(args ...string) *mongo.Client { // Check the connection err = client.Ping(context.TODO(), nil) if err != nil { - fmt.Printf("Failed to connect to MongoDB: %s\n", err) logrus.Fatal(err) errv := client.Disconnect(ctx) logrus.Fatal(errv) } - fmt.Println("Connected to MongoDB") logrus.Info("Connected to MongoDB!") return client @@ -71,7 +68,6 @@ func ConnectToDatabase(args ...string) *mongo.Client { // Create cache func CreateCache() *bigcache.BigCache { if GetEnv() == "devlocalhost" || GetEnv() == "devdeployed" || GetEnv() == "devlocalhostdocker" { - fmt.Printf("Cache is disabled in %s mode\n", GetEnv()) logrus.Printf("Cache is disabled in %s mode\n", GetEnv()) return nil } @@ -83,7 +79,6 @@ func CreateCache() *bigcache.BigCache { logrus.Fatal(err) } - fmt.Println("Cache created!") logrus.Info("Cache created!") return cache @@ -164,7 +159,6 @@ func CreateRabbitConnection() *amqp.Connection { // Connect to RabbitMQ conn, err := amqp.Dial(uri) if err != nil { - fmt.Printf("Failed to connect to RabbitMQ: %s\n", err) logrus.Fatal(err) } diff --git a/occupi-backend/coverage.out b/occupi-backend/coverage.out new file mode 100644 index 00000000..c42c0e72 --- /dev/null +++ b/occupi-backend/coverage.out @@ -0,0 +1,47 @@ +mode: set +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/OTP.go:9.36,12.16 3 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/OTP.go:12.16,14.3 1 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/OTP.go:15.2,16.21 2 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/response.go:16.9,22.2 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/response.go:30.9,37.2 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/response.go:46.9,56.2 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/response.go:59.34,69.2 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/response.go:79.9,90.2 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:22.20,27.16 3 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:27.16,29.3 1 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:33.2,42.35 3 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:46.42,50.16 4 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:50.16,52.3 1 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:53.2,55.17 3 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:59.34,62.16 3 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:62.16,64.3 1 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:65.2,66.19 2 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:70.44,73.16 3 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:73.16,75.3 1 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:77.2,79.19 2 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:83.94,89.31 4 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:89.31,91.25 2 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:91.25,93.62 2 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:93.62,97.5 3 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:102.2,104.20 2 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:108.41,111.2 2 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:114.39,118.2 2 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:121.45,130.23 2 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:130.23,132.3 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:134.2,134.44 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:134.44,136.3 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:138.2,138.44 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:138.44,140.3 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:142.2,142.34 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:142.34,144.3 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:146.2,146.40 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:146.40,148.3 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:150.2,150.13 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:154.35,158.2 2 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:161.52,167.16 2 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:167.16,169.3 1 0 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:170.2,170.18 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:174.80,178.16 2 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:178.16,180.3 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:181.2,181.19 1 1 +github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils/utils.go:184.20,187.2 0 0 diff --git a/occupi-backend/docker-compose.yml b/occupi-backend/docker-compose.yml new file mode 100644 index 00000000..8ef49c6e --- /dev/null +++ b/occupi-backend/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3.8' + +services: + occupi-backend: + build: + context: . + dockerfile: Dockerfile + container_name: occupi-backend + env_file: + - .env + ports: + - "8080:8080" + volumes: + - .:/go/src/COS301-SE-2024/occupi/occupi-backend + + occupi-nginx: + image: nginx:latest + container_name: occupi-nginx + ports: + - "13000:13000" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./cert.pem:/etc/nginx/ssl/cert.pem:ro + - ./key.pem:/etc/nginx/ssl/key.pem:ro + depends_on: + - occupi-backend \ No newline at end of file diff --git a/occupi-backend/nginx.conf b/occupi-backend/nginx.conf new file mode 100644 index 00000000..94b227fa --- /dev/null +++ b/occupi-backend/nginx.conf @@ -0,0 +1,30 @@ +events { + worker_connections 1024; +} + +http { + server { + listen 13000; + server_name localhost; + + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + + location / { + proxy_pass http://occupi-backend:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} \ No newline at end of file diff --git a/occupi-backend/notifications/main.go b/occupi-backend/notifications/main.go new file mode 100644 index 00000000..4fc3030c --- /dev/null +++ b/occupi-backend/notifications/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + expo "github.com/oliveroneill/exponent-server-sdk-golang/sdk" +) + +func main() { + // To check the token is valid + pushToken, err := expo.NewExponentPushToken("ExponentPushToken[5cpRYINQu42bhcKM5b7Vsb]") + if err != nil { + panic(err) + } + + // Create a new Expo SDK client + client := expo.NewPushClient(nil) + + // Publish message + response, err := client.Publish( + &expo.PushMessage{ + To: []expo.ExponentPushToken{pushToken}, + Body: "This is a test notification", + Data: map[string]string{"withSome": "data"}, + Sound: "default", + Title: "Notification Title", + Priority: expo.DefaultPriority, + }, + ) + + // Check errors + if err != nil { + panic(err) + } + + // Validate responses + if response.ValidateResponse() != nil { + fmt.Println(response.PushMessage.To, "failed") + } +} \ No newline at end of file diff --git a/occupi-backend/pkg/cache/cache.go b/occupi-backend/pkg/cache/cache.go index c5200ded..e82662a1 100644 --- a/occupi-backend/pkg/cache/cache.go +++ b/occupi-backend/pkg/cache/cache.go @@ -15,7 +15,7 @@ func GetUser(appsession *models.AppSession, email string) (models.User, error) { // unmarshal the user from the cache var user models.User - userData, err := appsession.Cache.Get(UserKey(email)) + userData, err := appsession.Cache.Get(email) if err != nil { logrus.Error("key does not exist: ", err) @@ -43,7 +43,7 @@ func SetUser(appsession *models.AppSession, user models.User) { } // set the user in the cache - if err := appsession.Cache.Set(UserKey(user.Email), userData); err != nil { + if err := appsession.Cache.Set(user.Email, userData); err != nil { logrus.Error("failed to set user in cache", err) return } @@ -55,7 +55,7 @@ func DeleteUser(appsession *models.AppSession, email string) { } // delete the user from the cache - if err := appsession.Cache.Delete(UserKey(email)); err != nil { + if err := appsession.Cache.Delete(email); err != nil { logrus.Error("failed to delete user from cache", err) return } @@ -68,7 +68,8 @@ func GetOTP(appsession *models.AppSession, email string, otp string) (models.OTP // unmarshal the otp from the cache var otpData models.OTP - otpDataBytes, err := appsession.Cache.Get(OTPKey(email, otp)) + otpKey := email + otp + otpDataBytes, err := appsession.Cache.Get(otpKey) if err != nil { logrus.Error("key does not exist: ", err) @@ -89,6 +90,7 @@ func SetOTP(appsession *models.AppSession, otpData models.OTP) { } // marshal the otp + otpKey := otpData.Email + otpData.OTP otpDataBytes, err := bson.Marshal(otpData) if err != nil { logrus.Error("failed to marshall", err) @@ -96,7 +98,7 @@ func SetOTP(appsession *models.AppSession, otpData models.OTP) { } // set the otp in the cache - if err := appsession.Cache.Set(OTPKey(otpData.Email, otpData.OTP), otpDataBytes); err != nil { + if err := appsession.Cache.Set(otpKey, otpDataBytes); err != nil { logrus.Error("failed to set otp in cache", err) return } @@ -108,7 +110,8 @@ func DeleteOTP(appsession *models.AppSession, email string, otp string) { } // delete the otp from the cache - if err := appsession.Cache.Delete(OTPKey(email, otp)); err != nil { + otpKey := email + otp + if err := appsession.Cache.Delete(otpKey); err != nil { logrus.Error("failed to delete otp from cache", err) return } diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index a06e894c..b8f64e4b 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -59,11 +59,6 @@ func GetAllData(ctx *gin.Context, appsession *models.AppSession) []bson.M { // attempts to save booking in database func SaveBooking(ctx *gin.Context, appsession *models.AppSession, booking models.Booking) (bool, error) { - // check if database is nil - if appsession.DB == nil { - logrus.Error("Database is nil") - return false, errors.New("database is nil") - } // Save the booking to the database collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("RoomBooking") _, err := collection.InsertOne(ctx, booking) @@ -71,42 +66,43 @@ func SaveBooking(ctx *gin.Context, appsession *models.AppSession, booking models logrus.Error(err) return false, err } - - cache.SetBooking(appsession, booking) - return true, nil } // Confirms the user check-in by checking certain criteria func ConfirmCheckIn(ctx *gin.Context, appsession *models.AppSession, checkIn models.CheckIn) (bool, error) { - // check if database is nil - if appsession.DB == nil { - logrus.Error("Database is nil") - return false, errors.New("database is nil") - } - // Save the check-in to the database collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("RoomBooking") - // Find the booking by bookingId, occupiId, and creator + // Find the booking by bookingId, roomId, and creator filter := bson.M{ - "occupiId": checkIn.BookingID, - "creator": checkIn.Creator, + "_id": checkIn.BookingID, + "creator": checkIn.Creator, } - update := bson.M{"$set": bson.M{"checkedIn": true}} - - _, err := collection.UpdateOne(ctx, filter, update) + // Find the booking + var booking models.Booking + err := collection.FindOne(context.TODO(), filter).Decode(&booking) if err != nil { - logrus.Error("Failed to update booking:", err) + if err == mongo.ErrNoDocuments { + logrus.Error("Booking not found") + return false, errors.New("booking not found") + } + logrus.Error("Failed to find booking:", err) return false, err } - if booking, err := cache.GetBooking(appsession, checkIn.BookingID); err == nil { - booking.CheckedIn = true - cache.SetBooking(appsession, booking) + update := bson.M{ + "$set": bson.M{"checkedIn": true}, } + opts := options.FindOneAndUpdate().SetReturnDocument(options.After) + var updatedBooking models.Booking + err = collection.FindOneAndUpdate(context.TODO(), filter, update, opts).Decode(&updatedBooking) + if err != nil { + logrus.Error("Failed to update booking:", err) + return false, err + } return true, nil } @@ -141,31 +137,16 @@ func EmailExists(ctx *gin.Context, appsession *models.AppSession, email string) // checks if booking exists in database func BookingExists(ctx *gin.Context, appsession *models.AppSession, id string) bool { - // check if database is nil - if appsession.DB == nil { - logrus.Error("Database is nil") - return false - } - - // Check if the booking exists in the cache if cache is not nil - if _, err := cache.GetBooking(appsession, id); err == nil { - return true - } - // Check if the booking exists in the database collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("RoomBooking") - filter := bson.M{"occupiId": id} + filter := bson.M{"_id": id} var existingbooking models.Booking err := collection.FindOne(ctx, filter).Decode(&existingbooking) if err != nil { logrus.Error(err) return false } - - // Add the booking to the cache if cache is not nil - cache.SetBooking(appsession, existingbooking) - return true } @@ -187,26 +168,7 @@ func AddUser(ctx *gin.Context, appsession *models.AppSession, user models.Regist NextVerificationDate: time.Now(), // this will be updated once the email is verified TwoFAEnabled: false, KnownLocations: []models.Location{}, - Details: models.Details{ - ImageID: "", - Name: "", - DOB: time.Now(), - Gender: "", - Pronouns: "", - }, - Notifications: models.Notifications{ - Invites: true, - BookingReminder: true, - }, - Security: models.Security{ - MFA: false, - Biometrics: false, - ForceLogout: false, - }, - Status: "", - Position: "", - DepartmentNo: "", - ExpoPushToken: user.ExpoPushToken, + ExpoPushToken: user.ExpoPushToken, } // Save the user to the database collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") @@ -305,6 +267,18 @@ func DeleteOTP(ctx *gin.Context, appsession *models.AppSession, email string, ot return true, nil } +// GetResetOTP retrieves the OTP for the given email and OTP from the database +func GetResetOTP(ctx context.Context, db *mongo.Client, email, otp string) (*models.OTP, error) { + collection := db.Database(configs.GetMongoDBName()).Collection("OTPs") + var resetOTP models.OTP + filter := bson.M{"email": email, "otp": otp} + err := collection.FindOne(ctx, filter).Decode(&resetOTP) + if err != nil { + return nil, err + } + return &resetOTP, nil +} + // verifies a user in the database func VerifyUser(ctx *gin.Context, appsession *models.AppSession, email string, ipAddress string) (bool, error) { // check if database is nil @@ -396,6 +370,11 @@ func CheckIfNextVerificationDateIsDue(ctx *gin.Context, appsession *models.AppSe if !time.Now().After(userData.NextVerificationDate) { return false, nil } + _, err := UpdateVerificationStatusTo(ctx, appsession, email, false) + if err != nil { + logrus.Error(err) + return false, err + } return true, nil } @@ -415,6 +394,11 @@ func CheckIfNextVerificationDateIsDue(ctx *gin.Context, appsession *models.AppSe if !time.Now().After(user.NextVerificationDate) { return false, nil } + _, err = UpdateVerificationStatusTo(ctx, appsession, email, false) + if err != nil { + logrus.Error(err) + return false, err + } return true, nil } @@ -476,40 +460,38 @@ func UpdateVerificationStatusTo(ctx *gin.Context, appsession *models.AppSession, // Confirms if a booking has been cancelled func ConfirmCancellation(ctx *gin.Context, appsession *models.AppSession, id string, email string) (bool, error) { - // check if database is nil - if appsession.DB == nil { - logrus.Error("Database is nil") - return false, errors.New("database is nil") - } // Save the check-in to the database collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("RoomBooking") // Find the booking by bookingId, roomId, and check if the email is in the emails object filter := bson.M{ - "occupiId": id, - "creator": email, + "_id": id, + "creator": email} + + // Find the booking + var localBooking models.Booking + err := collection.FindOne(context.TODO(), filter).Decode(&localBooking) + if err != nil { + if err == mongo.ErrNoDocuments { + logrus.Error("Email not associated with the room") + return false, errors.New("email not associated with the room") + } + logrus.Error("Failed to find booking:", err) + return false, err } // Delete the booking - _, err := collection.DeleteOne(ctx, filter) + _, err = collection.DeleteOne(context.TODO(), filter) if err != nil { logrus.Error("Failed to cancel booking:", err) return false, err } - - // delete booking from cache if cache is not nil - cache.DeleteBooking(appsession, id) - return true, nil } // Get user information func GetUserDetails(ctx *gin.Context, appsession *models.AppSession, email string) (models.UserDetailsRequest, error) { - // check if database is nil - if appsession.DB == nil { - logrus.Error("Database is nil") - return models.UserDetailsRequest{}, errors.New("database is nil") - } + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") // check if user is in cache if userData, err := cache.GetUser(appsession, email); err == nil { @@ -525,8 +507,6 @@ func GetUserDetails(ctx *gin.Context, appsession *models.AppSession, email strin }, nil } - collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") - filter := bson.M{"email": email} var user models.User err := collection.FindOne(ctx, filter).Decode(&user) @@ -658,7 +638,7 @@ func CheckIfUserIsAdmin(ctx *gin.Context, appsession *models.AppSession, email s return user.Role == constants.Admin, nil } -// AddResetToken adds a reset token to the database **Deprecated - Cannot confirm if this is still in use** +// AddResetToken adds a reset token to the database func AddResetToken(ctx context.Context, db *mongo.Client, email string, resetToken string, expirationTime time.Time) (bool, error) { collection := db.Database(configs.GetMongoDBName()).Collection("ResetTokens") resetTokenStruct := models.ResetToken{ @@ -674,7 +654,7 @@ func AddResetToken(ctx context.Context, db *mongo.Client, email string, resetTok return true, nil } -// retrieves the email associated with a reset token **Deprecated - Cannot confirm if this is still in use** +// retrieves the email associated with a reset token func GetEmailByResetToken(ctx context.Context, db *mongo.Client, resetToken string) (string, error) { collection := db.Database(configs.GetMongoDBName()).Collection("ResetTokens") filter := bson.M{"token": resetToken} @@ -687,7 +667,7 @@ func GetEmailByResetToken(ctx context.Context, db *mongo.Client, resetToken stri return resetTokenStruct.Email, nil } -// CheckResetToken function **Deprecated - Cannot confirm if this is still in use** +// CheckResetToken function func CheckResetToken(ctx *gin.Context, db *mongo.Client, email string, token string) (bool, error) { // Access the "ResetTokens" collection within the configs.GetMongoDBName() database. collection := db.Database(configs.GetMongoDBName()).Collection("ResetTokens") @@ -717,15 +697,15 @@ func CheckResetToken(ctx *gin.Context, db *mongo.Client, email string, token str } // UpdateUserPassword, which updates the password in the database set by the user -func UpdateUserPassword(ctx *gin.Context, appsession *models.AppSession, email string, password string) (bool, error) { +func UpdateUserPassword(ctx *gin.Context, db *mongo.Client, email string, password string) (bool, error) { // Check if the database is nil - if appsession.DB == nil { + if db == nil { logrus.Error("Database is nil") return false, errors.New("database is nil") } // Update the password in the database - collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") + collection := db.Database(configs.GetMongoDBName()).Collection("Users") filter := bson.M{"email": email} update := bson.M{"$set": bson.M{"password": password}} _, err := collection.UpdateOne(ctx, filter, update) @@ -735,15 +715,11 @@ func UpdateUserPassword(ctx *gin.Context, appsession *models.AppSession, email s } // Update users password in cache if cache is not nil - if userData, err := cache.GetUser(appsession, email); err == nil { - userData.Password = password - cache.SetUser(appsession, userData) - } return true, nil } -// ClearRestToekn, removes the reset token from the database **Deprecated - Cannot confirm if this is still in use** +// ClearRestToekn, removes the reset token from the database func ClearResetToken(ctx *gin.Context, db *mongo.Client, email string, token string) (bool, error) { // Delete the token from the database collection := db.Database(configs.GetMongoDBName()).Collection("ResetTokens") @@ -756,7 +732,7 @@ func ClearResetToken(ctx *gin.Context, db *mongo.Client, email string, token str return true, nil } -// ValidateResetToken validates the reset token **Deprecated - Cannot confirm if this is still in use** +// ValidateResetToken func ValidateResetToken(ctx context.Context, db *mongo.Client, email, token string) (bool, string, error) { // Find the reset token document var resetToken models.ResetToken @@ -1313,9 +1289,6 @@ func UploadImageData(ctx *gin.Context, appsession *models.AppSession, image mode return "", err } - // add image to cache - cache.SetImage(appsession, id.InsertedID.(primitive.ObjectID).Hex(), image) - return id.InsertedID.(primitive.ObjectID).Hex(), nil } @@ -1326,19 +1299,6 @@ func GetImageData(ctx *gin.Context, appsession *models.AppSession, imageID strin return models.Image{}, errors.New("database is nil") } - if imageData, err := cache.GetImage(appsession, imageID); err == nil { - resolutions := map[string][]byte{ - constants.ThumbnailRes: imageData.Thumbnail, - constants.LowRes: imageData.ImageLowRes, - constants.MidRes: imageData.ImageMidRes, - constants.HighRes: imageData.ImageHighRes, - } - - if data, ok := resolutions[quality]; ok && len(data) > 0 { - return imageData, nil - } - } - collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Images") id, err := primitive.ObjectIDFromHex(imageID) @@ -1361,21 +1321,6 @@ func GetImageData(ctx *gin.Context, appsession *models.AppSession, imageID strin return models.Image{}, err } - if imageData, err := cache.GetImage(appsession, imageID); err == nil { - switch quality { - case constants.ThumbnailRes: - imageData.Thumbnail = image.Thumbnail - case constants.LowRes: - imageData.ImageLowRes = image.ImageLowRes - case constants.MidRes: - imageData.ImageMidRes = image.ImageMidRes - case constants.HighRes: - imageData.ImageHighRes = image.ImageHighRes - } - - cache.SetImage(appsession, imageID, imageData) - } - return image, nil } @@ -1395,9 +1340,6 @@ func DeleteImageData(ctx *gin.Context, appsession *models.AppSession, imageID st return err } - // delete image from cache - cache.DeleteImage(appsession, imageID) - return nil } diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 22b387b8..db7e8e56 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -14,7 +14,6 @@ import ( "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" "github.com/go-playground/validator/v10" "github.com/sirupsen/logrus" - "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/mail" @@ -154,7 +153,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { return } - ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully booked!", booking.OccupiID)) + ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully booked!", booking.ID)) } func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { @@ -287,12 +286,6 @@ func UpdateUserDetails(ctx *gin.Context, appsession *models.AppSession) { return } - // if user is updating their email, create a new token for them - if user.Email != "" { - AttemptToSignNewEmail(ctx, appsession, user.Email) - return - } - ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully updated user details!", nil)) } @@ -389,22 +382,31 @@ func FilterCollection(ctx *gin.Context, appsession *models.AppSession, collectio } if collectionName == "RoomBooking" { - // check that the email field is set - if _, ok := filter.Filter["email"]; !ok { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Email must be provided", nil)) - return - } + // check that the .Filter in filter has the email key set and that the value is the same as the email in the appsession otherwise set the email key to the email in the appsession + if filter.Filter["email"] == nil || filter.Filter["email"] == "" { + email, err := AttemptToGetEmail(ctx, appsession) - // delete email field from the filter - email := filter.Filter["email"] - delete(filter.Filter, "email") + if err != nil { + logrus.Error("Failed to get email because: ", err) + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + return + } - // set emails field in the filter - filter.Filter["emails"] = bson.M{"$in": []string{email.(string)}} - } + filter.Filter["email"] = email + } else { + email, err := AttemptToGetEmail(ctx, appsession) - fmt.Printf("Filter: %v\n", filter) - fmt.Printf("Collection Name: %v\n", collectionName) + if err != nil { + logrus.Error("Failed to get email because: ", err) + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + return + } + + if filter.Filter["email"] != email { + filter.Filter["email"] = email + } + } + } res, totalResults, err := database.FilterCollectionWithProjection(ctx, appsession, collectionName, filter) @@ -525,8 +527,8 @@ func UpdateSecuritySettings(ctx *gin.Context, appsession *models.AppSession) { // Validate the given passwords if they exist if securitySettings.CurrentPassword != "" && securitySettings.NewPassword != "" && securitySettings.NewPasswordConfirm != "" { - securitySetting, err, success := SanitizeSecuritySettingsPassword(ctx, appsession, securitySettings) - if err != nil || !success { + securitySetting, err := SanitizeSecuritySettingsPassword(ctx, appsession, securitySettings) + if err != nil { logrus.Error("Failed to sanitize security settings because: ", err) return } @@ -835,16 +837,10 @@ func DownloadProfileImage(ctx *gin.Context, appsession *models.AppSession) { switch request.Quality { case constants.ThumbnailRes: ctx.Data(http.StatusOK, "application/octet-stream", imageData.Thumbnail) - - go PreloadAllImageResolutions(ctx, appsession, id) case constants.LowRes: ctx.Data(http.StatusOK, "application/octet-stream", imageData.ImageLowRes) - - go PreloadMidAndHighResolutions(ctx, appsession, id) case constants.MidRes: ctx.Data(http.StatusOK, "application/octet-stream", imageData.ImageMidRes) - - go PreloadHighResolution(ctx, appsession, id) case constants.HighRes: ctx.Data(http.StatusOK, "application/octet-stream", imageData.ImageHighRes) default: @@ -964,36 +960,3 @@ func UploadImage(ctx *gin.Context, appsession *models.AppSession, roomUpload boo ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully uploaded image!", gin.H{"id": newID})) } - -func AddRoom(ctx *gin.Context, appsession *models.AppSession) { - var room models.RequestRoom - if err := ctx.ShouldBindJSON(&room); err != nil { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse( - http.StatusBadRequest, - "Invalid request payload", - constants.InvalidRequestPayloadCode, - "Invalid JSON payload", - nil)) - return - } - - // Save the room to the database - roomID, err := database.AddRoom(ctx, appsession, room) - if err != nil { - var msg string - if err.Error() == "room already exists" { - msg = "Room already exists" - } else { - msg = "Failed to add room" - } - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse( - http.StatusInternalServerError, - "Failed to add room", - constants.InternalServerErrorCode, - "Failed to add room", - gin.H{"message": msg})) - return - } - - ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully added room!", gin.H{"roomid": roomID})) -} diff --git a/occupi-backend/pkg/handlers/api_helpers.go b/occupi-backend/pkg/handlers/api_helpers.go deleted file mode 100644 index af110000..00000000 --- a/occupi-backend/pkg/handlers/api_helpers.go +++ /dev/null @@ -1,77 +0,0 @@ -package handlers - -import ( - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" - "github.com/gin-gonic/gin" -) - -func PreloadAllImageResolutions(ctx *gin.Context, appsession *models.AppSession, id string) { - // ***IMPORTANT NOTE ABOUT ERROR HANDLING IN THIS FUNCTION*** - // if an error occurs, its not a big deal - // this mainly serves as an attempt to cache the image - // so that it can be served faster when the user requests it - // if we cannot, we can just serve the image from the database - // the user will just have to wait a bit longer - - go func() { - _, err := database.GetImageData(ctx, appsession, id, constants.LowRes) - if err != nil { - return - } - }() - - go func() { - _, err := database.GetImageData(ctx, appsession, id, constants.MidRes) - if err != nil { - return - } - }() - - go func() { - _, err := database.GetImageData(ctx, appsession, id, constants.HighRes) - if err != nil { - return - } - }() -} - -func PreloadMidAndHighResolutions(ctx *gin.Context, appsession *models.AppSession, id string) { - // ***IMPORTANT NOTE ABOUT ERROR HANDLING IN THIS FUNCTION*** - // if an error occurs, its not a big deal - // this mainly serves as an attempt to cache the image - // so that it can be served faster when the user requests it - // if we cannot, we can just serve the image from the database - // the user will just have to wait a bit longer - - go func() { - _, err := database.GetImageData(ctx, appsession, id, constants.MidRes) - if err != nil { - return - } - }() - - go func() { - _, err := database.GetImageData(ctx, appsession, id, constants.HighRes) - if err != nil { - return - } - }() -} - -func PreloadHighResolution(ctx *gin.Context, appsession *models.AppSession, id string) { - // ***IMPORTANT NOTE ABOUT ERROR HANDLING IN THIS FUNCTION*** - // if an error occurs, its not a big deal - // this mainly serves as an attempt to cache the image - // so that it can be served faster when the user requests it - // if we cannot, we can just serve the image from the database - // the user will just have to wait a bit longer - - go func() { - _, err := database.GetImageData(ctx, appsession, id, constants.HighRes) - if err != nil { - return - } - }() -} diff --git a/occupi-backend/pkg/handlers/auth_handlers.go b/occupi-backend/pkg/handlers/auth_handlers.go index 31122907..2f435c12 100644 --- a/occupi-backend/pkg/handlers/auth_handlers.go +++ b/occupi-backend/pkg/handlers/auth_handlers.go @@ -47,7 +47,8 @@ func Login(ctx *gin.Context, appsession *models.AppSession, role string, cookies // validate email exists if valid, err := ValidateEmailExists(ctx, appsession, requestUser.Email); !valid { if err != nil { - logrus.WithError(err).Error("Error validating email") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) } return } @@ -55,7 +56,8 @@ func Login(ctx *gin.Context, appsession *models.AppSession, role string, cookies // validate password if valid, err := ValidatePasswordCorrectness(ctx, appsession, requestUser); !valid { if err != nil { - logrus.WithError(err).Error("Error validating password") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) } return } @@ -63,7 +65,8 @@ func Login(ctx *gin.Context, appsession *models.AppSession, role string, cookies // pre-login checks if success, err := PreLoginAccountChecks(ctx, appsession, requestUser.Email, role); !success { if err != nil { - logrus.WithError(err).Error("Error validating email") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) } return } @@ -72,7 +75,8 @@ func Login(ctx *gin.Context, appsession *models.AppSession, role string, cookies token, expirationTime, err := GenerateJWTTokenAndStartSession(ctx, appsession, requestUser.Email, role) if err != nil { - logrus.WithError(err).Error("Error generating JWT token") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) return } @@ -344,7 +348,8 @@ func Register(ctx *gin.Context, appsession *models.AppSession) { // validate password if valid, err := ValidatePasswordEntry(ctx, appsession, requestUser.Password); !valid { if err != nil { - logrus.WithError(err).Error("Error validating password") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) } return } @@ -352,7 +357,8 @@ func Register(ctx *gin.Context, appsession *models.AppSession) { // validate email exists if valid, err := ValidateEmailDoesNotExist(ctx, appsession, requestUser.Email); !valid { if err != nil { - logrus.WithError(err).Error("Error validating email") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) } return } @@ -373,8 +379,10 @@ func Register(ctx *gin.Context, appsession *models.AppSession) { return } - if _, err := SendOTPEmail(ctx, appsession, requestUser.Email, constants.VerifyEmail); err != nil { - logrus.WithError(err).Error("Error sending OTP email") + _, err = SendOTPEmail(ctx, appsession, requestUser.Email, constants.VerifyEmail) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) return } } @@ -395,7 +403,8 @@ func ResendOTP(ctx *gin.Context, appsession *models.AppSession, resendType strin // validate email exists if valid, err := ValidateEmailExists(ctx, appsession, request.Email); !valid { if err != nil { - logrus.WithError(err).Error("Error validating email") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) } return } @@ -410,12 +419,17 @@ func ResendOTP(ctx *gin.Context, appsession *models.AppSession, resendType strin emailType = constants.VerifyEmail } // sned the otp to verify the email - if _, err := SendOTPEmail(ctx, appsession, request.Email, emailType); err != nil { - logrus.WithError(err).Error("Error sending OTP email") + _, err := SendOTPEmail(ctx, appsession, request.Email, emailType) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) return } - // SendOTPEmail has logic that will send back a json response so no need to worry about the logic here + ctx.JSON(http.StatusOK, utils.SuccessResponse( + http.StatusOK, + "Please check your email for the OTP to verify your account.", + nil)) } // handler for verifying a users otp /api/verify-otp @@ -434,15 +448,23 @@ func VerifyOTP(ctx *gin.Context, appsession *models.AppSession, login bool, role // validate email exists if valid, err := ValidateEmailExists(ctx, appsession, userotp.Email); !valid { if err != nil { - logrus.WithError(err).Error("Error validating email") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) } return } if valid, err := ValidateOTPExists(ctx, appsession, userotp.Email, userotp.OTP); !valid { if err != nil { - logrus.WithError(err).Error("Error validating otp") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) } + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse( + http.StatusBadRequest, + "Invalid OTP", + constants.InvalidAuthCode, + "Otp expired or invalid", + nil)) return } @@ -466,14 +488,14 @@ func VerifyOTP(ctx *gin.Context, appsession *models.AppSession, login bool, role http.StatusOK, "Email verified successfully!", nil)) - return } // generate a jwt token for the user token, expirationTime, err := GenerateJWTTokenAndStartSession(ctx, appsession, userotp.Email, role) if err != nil { - logrus.WithError(err).Error("Error generating JWT token") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) return } @@ -589,9 +611,11 @@ func ResetPassword(ctx *gin.Context, appsession *models.AppSession, role string, } // Validate email - if valid, err := ValidateEmailExists(ctx, appsession, resetRequest.Email); !valid { + valid, err := ValidateEmailExists(ctx, appsession, resetRequest.Email) + if !valid { if err != nil { - logrus.WithError(err).Error("Error validating email") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) } return } @@ -599,15 +623,27 @@ func ResetPassword(ctx *gin.Context, appsession *models.AppSession, role string, // Validate OTP if valid, err := ValidateOTPExists(ctx, appsession, resetRequest.Email, resetRequest.OTP); !valid { if err != nil { - logrus.WithError(err).Error("Error validating OTP") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) } + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse( + http.StatusBadRequest, + "Invalid OTP", + constants.InvalidAuthCode, + "OTP expired or invalid", + nil)) return } // Validate new password password, err := ValidatePasswordEntryAndReturnHash(ctx, appsession, resetRequest.NewPassword) - if err != nil || password == "" { - logrus.WithError(err).Error("Error validating password") + if err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse( + http.StatusBadRequest, + "Password validation failed", + "ValidationErrorCode", + "Password does not meet requirements", + nil)) return } @@ -619,7 +655,7 @@ func ResetPassword(ctx *gin.Context, appsession *models.AppSession, role string, } // Update password in database - success, err := database.UpdateUserPassword(ctx, appsession, resetRequest.Email, password) + success, err := database.UpdateUserPassword(ctx, appsession.DB, resetRequest.Email, password) if err != nil || !success { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse( http.StatusInternalServerError, @@ -633,7 +669,12 @@ func ResetPassword(ctx *gin.Context, appsession *models.AppSession, role string, // Log the user in and Generate a JWT token token, exp, err := GenerateJWTTokenAndStartSession(ctx, appsession, resetRequest.Email, role) if err != nil { - logrus.WithError(err).Error("Error generating JWT token") + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse( + http.StatusInternalServerError, + "Token generation failed", + constants.InternalServerErrorCode, + "Unable to generate a token for the user", + nil)) return } @@ -682,7 +723,8 @@ func IsEmailVerified(ctx *gin.Context, appsession *models.AppSession) { // validate email exists if valid, err := ValidateEmailExists(ctx, appsession, request.Email); !valid { if err != nil { - logrus.WithError(err).Error("Error validating email") + ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) + logrus.Error(err) } return } diff --git a/occupi-backend/pkg/handlers/auth_helpers.go b/occupi-backend/pkg/handlers/auth_helpers.go index 9a490a44..9cb4d95d 100644 --- a/occupi-backend/pkg/handlers/auth_helpers.go +++ b/occupi-backend/pkg/handlers/auth_helpers.go @@ -1,10 +1,10 @@ package handlers import ( + "errors" "net/http" "time" - "github.com/COS301-SE-2024/occupi/occupi-backend/configs" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database" @@ -21,13 +21,11 @@ func SendOTPEmail(ctx *gin.Context, appsession *models.AppSession, email string, // generate a random otp for the user and send email otp, err := utils.GenerateOTP() if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return false, err } // save otp to database if _, err := database.AddOTP(ctx, appsession, email, otp); err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return false, err } @@ -53,14 +51,9 @@ func SendOTPEmail(ctx *gin.Context, appsession *models.AppSession, email string, } if err := mail.SendMail(email, subject, body); err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return false, err } - ctx.JSON(http.StatusOK, utils.SuccessResponse( - http.StatusOK, - "Please check your email for an otp.", - nil)) return true, nil } @@ -68,13 +61,11 @@ func SendOTPEMailForIPInfo(ctx *gin.Context, appsession *models.AppSession, emai // generate a random otp for the user and send email otp, err := utils.GenerateOTP() if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return false, err } // save otp to database if _, err := database.AddOTP(ctx, appsession, email, otp); err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return false, err } @@ -82,7 +73,6 @@ func SendOTPEMailForIPInfo(ctx *gin.Context, appsession *models.AppSession, emai body := utils.FormatIPAddressConfirmationEmailBodyWithIPInfo(otp, email, unrecognizedLogger) if err := mail.SendMail(email, subject, body); err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return false, err } @@ -106,14 +96,12 @@ func GenerateJWTTokenAndStartSession(ctx *gin.Context, appsession *models.AppSes } if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return "", time.Time{}, err } err = utils.SetSession(ctx, claims) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return "", time.Time{}, err } @@ -132,7 +120,7 @@ func ValidatePasswordEntry(ctx *gin.Context, appsession *models.AppSession, pass constants.InvalidRequestPayloadCode, "Password does neet meet requirements", nil)) - return false, nil + return false, errors.New("invalid password") } return true, nil @@ -150,7 +138,7 @@ func ValidatePasswordEntryAndReturnHash(ctx *gin.Context, appsession *models.App constants.InvalidRequestPayloadCode, "Password does neet meet requirements", nil)) - return "", nil + return "", errors.New("invalid password") } password, err := utils.Argon2IDHash(password) @@ -175,20 +163,18 @@ func ValidatePasswordCorrectness(ctx *gin.Context, appsession *models.AppSession constants.InvalidRequestPayloadCode, "Password does neet meet requirements", nil)) - return false, nil + return false, errors.New("invalid password") } // fetch hashed password hashedPassword, err := database.GetPassword(ctx, appsession, requestUser.Email) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return false, err } // check if they match match, err := utils.CompareArgon2IDHash(requestUser.Password, hashedPassword) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return false, err } @@ -199,7 +185,7 @@ func ValidatePasswordCorrectness(ctx *gin.Context, appsession *models.AppSession constants.InvalidAuthCode, "Password is incorrect", nil)) - return false, nil + return false, errors.New("password is incorrect") } return true, nil @@ -295,7 +281,6 @@ func PreLoginAccountChecks(ctx *gin.Context, appsession *models.AppSession, emai // check if the user is verified verified, err := database.CheckIfUserIsVerified(ctx, appsession, email) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return false, err } @@ -313,7 +298,6 @@ func PreLoginAccountChecks(ctx *gin.Context, appsession *models.AppSession, emai if role == constants.Admin { isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession, email) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return false, err } @@ -331,7 +315,6 @@ func PreLoginAccountChecks(ctx *gin.Context, appsession *models.AppSession, emai // check if the next verification date is due isVerificationDue, err := database.CheckIfNextVerificationDateIsDue(ctx, appsession, email) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return false, err } @@ -339,7 +322,6 @@ func PreLoginAccountChecks(ctx *gin.Context, appsession *models.AppSession, emai isIPValid, unrecognizedLogger, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, utils.GetClientIP(ctx)) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return false, err } @@ -347,41 +329,26 @@ func PreLoginAccountChecks(ctx *gin.Context, appsession *models.AppSession, emai mfaEnabled, err := database.CheckIfUserHasMFAEnabled(ctx, appsession, email) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) return false, err } - switch { - case isVerificationDue: - // update verification status in database to false - _, err = database.UpdateVerificationStatusTo(ctx, appsession, email, false) + if isVerificationDue || mfaEnabled { + _, err := SendOTPEmail(ctx, appsession, email, constants.ReverifyEmail) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) - return false, err - } - if _, err := SendOTPEmail(ctx, appsession, email, constants.ReverifyEmail); err != nil { return false, err } return false, nil - - case mfaEnabled: - if _, err := SendOTPEmail(ctx, appsession, email, constants.ReverifyEmail); err != nil { - return false, err - } - return false, nil - - case !isIPValid: - if _, err := SendOTPEMailForIPInfo(ctx, appsession, email, constants.ConfirmIPAddress, unrecognizedLogger); err != nil { + } else if !isIPValid { + _, err := SendOTPEMailForIPInfo(ctx, appsession, email, constants.ConfirmIPAddress, unrecognizedLogger) + if err != nil { return false, err } return false, nil - - default: - return true, nil } + return true, nil } -func SanitizeSecuritySettingsPassword(ctx *gin.Context, appsession *models.AppSession, securitySettings models.SecuritySettingsRequest) (models.SecuritySettingsRequest, error, bool) { +func SanitizeSecuritySettingsPassword(ctx *gin.Context, appsession *models.AppSession, securitySettings models.SecuritySettingsRequest) (models.SecuritySettingsRequest, error) { // sanitize input securitySettings.Email = utils.SanitizeInput(securitySettings.Email) securitySettings.CurrentPassword = utils.SanitizeInput(securitySettings.CurrentPassword) @@ -398,7 +365,7 @@ func SanitizeSecuritySettingsPassword(ctx *gin.Context, appsession *models.AppSe constants.InvalidRequestPayloadCode, "Password does neet meet requirements", nil)) - return models.SecuritySettingsRequest{}, nil, false + return models.SecuritySettingsRequest{}, errors.New("invalid password") } // check if the passwords match @@ -409,7 +376,7 @@ func SanitizeSecuritySettingsPassword(ctx *gin.Context, appsession *models.AppSe constants.InvalidRequestPayloadCode, "Passwords do not match", nil)) - return models.SecuritySettingsRequest{}, nil, false + return models.SecuritySettingsRequest{}, errors.New("passwords do not match") } // check if the current password is correct @@ -417,14 +384,14 @@ func SanitizeSecuritySettingsPassword(ctx *gin.Context, appsession *models.AppSe if err != nil { ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) - return models.SecuritySettingsRequest{}, err, false + return models.SecuritySettingsRequest{}, err } match, err := utils.CompareArgon2IDHash(securitySettings.CurrentPassword, password) if err != nil { ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) - return models.SecuritySettingsRequest{}, err, false + return models.SecuritySettingsRequest{}, err } if !match { @@ -434,7 +401,7 @@ func SanitizeSecuritySettingsPassword(ctx *gin.Context, appsession *models.AppSe constants.InvalidAuthCode, "Password is incorrect", nil)) - return models.SecuritySettingsRequest{}, nil, false + return models.SecuritySettingsRequest{}, errors.New("password is incorrect") } // hash the new password @@ -442,12 +409,12 @@ func SanitizeSecuritySettingsPassword(ctx *gin.Context, appsession *models.AppSe if err != nil { ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) - return models.SecuritySettingsRequest{}, err, false + return models.SecuritySettingsRequest{}, err } securitySettings.NewPassword = hashedPassword - return securitySettings, nil, true + return securitySettings, nil } // AllocateAuthTokens decides whether to send the JWT token in the Authorization header or as a cookie based on a condition. @@ -484,64 +451,3 @@ func AttemptToGetEmail(ctx *gin.Context, appsession *models.AppSession) (string, return claims.Email, nil } } - -func AttemptToSignNewEmail(ctx *gin.Context, appsession *models.AppSession, email string) { - claims, err := utils.GetClaimsFromCTX(ctx) - - if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) - return - } - - _ = utils.ClearSession(ctx) - - // Clear the Authorization header - ctx.Header("Authorization", "") - - // Alternatively, completely remove the Authorization header - ctx.Writer.Header().Del("Authorization") - - // List of domains to clear cookies from - domains := configs.GetOccupiDomains() - - // Iterate over each domain and clear the "token" and "occupi-sessions-store" cookies - for _, domain := range domains { - ctx.SetCookie("token", "", -1, "/", domain, false, true) - ctx.SetCookie("occupi-sessions-store", "", -1, "/", domain, false, true) - } - - // generate a jwt token for the user - token, expirationTime, err := GenerateJWTTokenAndStartSession(ctx, appsession, email, claims.Role) - - if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) - return - } - - originToken := ctx.GetString("tokenOrigin") - var cookies bool - - if originToken == "cookie" { - cookies = true - } else { - cookies = false - } - - if !cookies { - // Send the JWT token in the Authorization header - ctx.Header("Authorization", "Bearer "+token) - ctx.JSON(http.StatusOK, utils.SuccessResponse( - http.StatusOK, - "Successfully updated user details!", - gin.H{"token": token}, - )) - } else { - // Set the JWT token in a cookie - ctx.SetCookie("token", token, int(time.Until(expirationTime).Seconds()), "/", "", false, true) - ctx.JSON(http.StatusOK, utils.SuccessResponse( - http.StatusOK, - "Successfully updated user details!", - nil, - )) - } -} diff --git a/occupi-backend/pkg/handlers/callback_handlers.go b/occupi-backend/pkg/handlers/callback_handlers.go new file mode 100644 index 00000000..1a95cec8 --- /dev/null +++ b/occupi-backend/pkg/handlers/callback_handlers.go @@ -0,0 +1,53 @@ +package handlers + +import ( + "net/http" + + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// Handler for our callback. +func CallbackHandler(c *gin.Context, appsession *models.AppSession) { + session := sessions.Default(c) + if c.Query("state") != session.Get("state") { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + logrus.Error("Invalid state parameter.") + return + } + + // Exchange an authorization code for a token. + token, err := appsession.Authenticator.Exchange(c.Request.Context(), c.Query("code")) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) + logrus.Error(err) + return + } + + idToken, err := appsession.Authenticator.VerifyIDToken(c.Request.Context(), token) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify ID Token."}) + logrus.Error(err) + return + } + + var profile map[string]interface{} + if err := idToken.Claims(&profile); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unmarshal ID Token."}) + logrus.Error(err) + return + } + + session.Set("access_token", token.AccessToken) + session.Set("profile", profile) + if err := session.Save(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) + logrus.Error(err) + return + } + + // Redirect to logged in page. + c.Redirect(http.StatusTemporaryRedirect, "/api/resource-auth") +} diff --git a/occupi-backend/pkg/mail/email_format.go b/occupi-backend/pkg/mail/email_format.go new file mode 100644 index 00000000..cb799aab --- /dev/null +++ b/occupi-backend/pkg/mail/email_format.go @@ -0,0 +1,210 @@ +package mail + +import "strconv" + +// formats booking email body +func FormatBookingEmailBody(bookingID string, roomID string, slot int) string { + return ` + Dear User, + + Thank you for booking with Occupi. Here are your booking details: + + Booking ID: ` + bookingID + ` + Room ID: ` + roomID + ` + Slot: ` + strconv.Itoa(slot) + ` + + If you have any questions, feel free to contact us. + + Thank you, + The Occupi Team + ` +} + +// formats booking email body to send person who booked +func FormatBookingEmailBodyForBooker(bookingID string, roomID string, slot int, attendees []string, email string) string { + listOfAttendees := "
    " + for _, email := range attendees { + listOfAttendees += "
  • " + email + "
  • " + } + listOfAttendees += "
" + + return appendHeader("Booking") + ` +
+

Dear booker,

+

+ You have successfully booked an office space. Here are the booking details:

+ Booking ID: ` + bookingID + `
+ Room ID: ` + roomID + `
+ Slot: ` + strconv.Itoa(slot) + `

+ Attendees:` + listOfAttendees + `

+ Please ensure you arrive on time for your booking.

+ Thank you,
+ The Occupi Team
+

+
` + appendFooter() +} + +// formats cancellation email body to send person who booked +func FormatCancellationEmailBodyForBooker(bookingID string, roomID string, slot int, email string) string { + + return appendHeader("Cancellation") + ` +
+

Dear booker,

+

+ You have successfully cancelled your booked office space. Here are the booking details:

+ Booking ID: ` + bookingID + `
+ Room ID: ` + roomID + `
+ Slot: ` + strconv.Itoa(slot) + `

+ Thank you,
+ The Occupi Team
+

+
` + appendFooter() +} + +// formats booking email body to send attendees +func FormatBookingEmailBodyForAttendees(bookingID string, roomID string, slot int, email string) string { + return appendHeader("Booking") + ` +
+

Dear attendees,

+

+ ` + email + ` has booked an office space and invited you to join. Here are the booking details:

+ Booking ID: ` + bookingID + `
+ Room ID: ` + roomID + `
+ Slot: ` + strconv.Itoa(slot) + `

+ If you have any questions, feel free to contact us.

+ Thank you,
+ The Occupi Team
+

+
` + appendFooter() +} + +// formats cancellation email body to send attendees +func FormatCancellationEmailBodyForAttendees(bookingID string, roomID string, slot int, email string) string { + return appendHeader("Booking") + ` +
+

Dear attendees,

+

+ ` + email + ` has cancelled the booked office space with the following details:

+ Booking ID: ` + bookingID + `
+ Room ID: ` + roomID + `
+ Slot: ` + strconv.Itoa(slot) + `

+ If you have any questions, feel free to contact us.

+ Thank you,
+ The Occupi Team
+

+
` + appendFooter() +} + +// formats verification email body +func FormatEmailVerificationBody(otp string, email string) string { + return appendHeader("Registration") + ` +
+

Dear ` + email + `,

+

+ Thank you for registering with Occupi.

+ To complete your registration, please use the following One-Time Password (OTP) to verify your email address:
+ OTP: ` + otp + `
+ This OTP is valid for the next 10 minutes. Please do not share this OTP with anyone for security reasons.

+ If you did not request this email, please disregard it.

+ Thank you,
+ The Occupi Team
+

+
` + appendFooter() +} + +// formats re - verification email body +func FormatReVerificationEmailBody(otp string, email string) string { + return appendHeader("Re-verification") + ` +
+

Dear ` + email + `,

+

+ Thank you for using Occupi.

+ To verify your email address, please use the following One-Time Password (OTP):
+ OTP: ` + otp + `
+ This OTP is valid for the next 10 minutes. Please do not share this OTP with anyone for security reasons.

+ If you did not request this email, please disregard it.

+ Thank you,
+ The Occupi Team
+

+
` + appendFooter() +} + +func appendHeader(title string) string { + return ` + + + + + + ` + title + ` + + + +
+

Occupi ` + title + `

+
+ ` +} + +func appendFooter() string { + return ` + + + + ` +} + + +// FormatPasswordResetEmailBody(otp, email) +func FormatResetPasswordEmailBody(otp string, email string) string { + return appendHeader("Password Reset") + ` +
+

Dear ` + email + `,

+

+ You have requested to reset your password. Your One-Time Password (OTP) is:
+

` + otp + `



+ Please use this OTP to reset your password. If you did not request this email, please ignore it.

+ This OTP will expire in 10 minutes.

+ Thank you,
+ The Occupi Team
+

+
` + appendFooter() +} + +// formatTwoFAEmailBody +func FormatTwoFAEmailBody(otp string, email string) string { + return appendHeader("Two-Factor Authentication") + ` +
+

Dear ` + email + `,

+

+ You have requested to enable Two-Factor Authentication. Your One-Time Password (OTP) is:
+

` + otp + `



+ Please use this OTP to enable Two-Factor Authentication. If you did not request this email, please ignore it.

+ This OTP will expire in 10 minutes.

+ Thank you,
+ The Occupi Team
+

+
` + appendFooter() +} \ No newline at end of file diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index 866a490d..b5c3fd50 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -133,7 +133,6 @@ type Room struct { Description string `json:"description" bson:"description"` RoomName string `json:"roomName" bson:"roomName"` RoomImageIDs []string `json:"roomImageIds" bson:"roomImageIds"` - Resources []string `json:"resources" bson:"resources"` } type ResetToken struct { diff --git a/occupi-backend/pkg/router/router.go b/occupi-backend/pkg/router/router.go index 8a3abecd..b5a30e0e 100644 --- a/occupi-backend/pkg/router/router.go +++ b/occupi-backend/pkg/router/router.go @@ -54,7 +54,6 @@ func OccupiRouter(router *gin.Engine, appsession *models.AppSession) { api.GET("/image/:id", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.DownloadImage(ctx, appsession) }) api.POST("/upload-image", middleware.ProtectedRoute, middleware.AdminRoute, middleware.LimitRequestBodySize(16<<20), func(ctx *gin.Context) { handlers.UploadImage(ctx, appsession, false) }) api.POST("/upload-room-image", middleware.ProtectedRoute, middleware.AdminRoute, middleware.LimitRequestBodySize(16<<20), func(ctx *gin.Context) { handlers.UploadImage(ctx, appsession, true) }) - api.PUT("/add-room", middleware.ProtectedRoute, middleware.AdminRoute, func(ctx *gin.Context) { handlers.AddRoom(ctx, appsession) }) } auth := router.Group("/auth") { diff --git a/occupi-backend/pkg/sender/send.go b/occupi-backend/pkg/sender/send.go index 338aef63..b808453f 100644 --- a/occupi-backend/pkg/sender/send.go +++ b/occupi-backend/pkg/sender/send.go @@ -15,7 +15,7 @@ import ( func PublishMessage(appsession *models.AppSession, notification models.ScheduledNotification) error { // if gin run mode is test randomly return error if configs.GetGinRunMode() == "test" { - return nil + return utils.RandomError() } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) diff --git a/occupi-backend/pkg/utils/utils.go b/occupi-backend/pkg/utils/utils.go index 8fe349d8..69eb1c31 100644 --- a/occupi-backend/pkg/utils/utils.go +++ b/occupi-backend/pkg/utils/utils.go @@ -351,7 +351,7 @@ func SantizeProjection(queryInput models.QueryInput) []string { // Remove password field from projection if present sanitizedProjection := []string{} for _, field := range queryInput.Projection { - if field != "password" && field != "unsentExpoPushTokens" { + if field != "password" && field != "unsentExpoPushTokens" && field != "emails" { sanitizedProjection = append(sanitizedProjection, field) } } @@ -364,6 +364,7 @@ func ConstructProjection(queryInput models.QueryInput, sanitizedProjection []str if queryInput.Projection == nil || len(queryInput.Projection) == 0 { projection["password"] = 0 // Exclude password by default projection["unsentExpoPushTokens"] = 0 + projection["emails"] = 0 } else { for _, field := range sanitizedProjection { switch field { @@ -371,6 +372,8 @@ func ConstructProjection(queryInput models.QueryInput, sanitizedProjection []str projection[field] = 0 case "unsentExpoPushTokens": projection[field] = 0 + case "emails": + projection[field] = 0 default: projection[field] = 1 } @@ -456,7 +459,7 @@ func ConvertToStringArray(input interface{}) []string { } func ConvertTokensToStringArray(tokens []primitive.M, key string) ([]string, error) { - stringArray := []string{} + var stringArray []string for _, token := range tokens { // Ensure the map contains the key @@ -507,12 +510,8 @@ func GetClaimsFromCTX(ctx *gin.Context) (*authenticator.Claims, error) { return nil, errors.New("no token provided") } - // set in ctx origin of token, whether it was from cookie or header - ctx.Set("tokenOrigin", "cookie") - if tokenStr == "" { tokenStr = headertokenStr - ctx.Set("tokenOrigin", "header") } claims, err := authenticator.ValidateToken(tokenStr) diff --git a/occupi-backend/tests/cache_methods_unit_test.go b/occupi-backend/tests/cache_methods_unit_test.go index c49c7cf3..58e20e79 100644 --- a/occupi-backend/tests/cache_methods_unit_test.go +++ b/occupi-backend/tests/cache_methods_unit_test.go @@ -12,43 +12,6 @@ import ( "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" ) -func TestUserKey(t *testing.T) { - email := "test@example.com" - expected := "Users:test@example.com" - result := cache.UserKey(email) - if result != expected { - t.Errorf("UserKey(%s) = %s; want %s", email, result, expected) - } -} - -func TestOTPKey(t *testing.T) { - email := "test@example.com" - otp := "123456" - expected := "OTPs:test@example.com:123456" - result := cache.OTPKey(email, otp) - if result != expected { - t.Errorf("OTPKey(%s, %s) = %s; want %s", email, otp, result, expected) - } -} - -func TestRoomBookingKey(t *testing.T) { - roomID := "room123" - expected := "RoomBookings:room123" - result := cache.RoomBookingKey(roomID) - if result != expected { - t.Errorf("RoomBookingKey(%s) = %s; want %s", roomID, result, expected) - } -} - -func TestImageKey(t *testing.T) { - imageID := "image123" - expected := "Images:image123" - result := cache.ImageKey(imageID) - if result != expected { - t.Errorf("ImageKey(%s) = %s; want %s", imageID, result, expected) - } -} - func TestGetUser(t *testing.T) { email := "test@example.com" user := models.User{Email: email} @@ -89,7 +52,7 @@ func TestGetUser(t *testing.T) { appsession = &models.AppSession{ Cache: configs.CreateCache(), } - err := appsession.Cache.Set(cache.UserKey(email), userData) + err := appsession.Cache.Set(email, userData) assert.NoError(t, err) } else { @@ -146,7 +109,7 @@ func TestSetUser(t *testing.T) { if tt.name != "cache is nil" { // check if user was set in cache - userData, err := appsession.Cache.Get(cache.UserKey(email)) + userData, err := appsession.Cache.Get(email) assert.NoError(t, err) // unmarshal the user from the cache @@ -180,11 +143,6 @@ func TestDeleteUser(t *testing.T) { email: email, expectedUser: models.User{}, }, - { - name: "cache key does not exist", - email: "doesnotexist@example.com", - expectedUser: models.User{}, - }, } for _, tt := range tests { @@ -198,7 +156,7 @@ func TestDeleteUser(t *testing.T) { // add user to cache userData, _ := bson.Marshal(user) - err := appsession.Cache.Set(cache.UserKey(email), userData) + err := appsession.Cache.Set(email, userData) assert.NoError(t, err) } else { @@ -207,20 +165,13 @@ func TestDeleteUser(t *testing.T) { cache.DeleteUser(appsession, tt.email) - if tt.name != "cache is nil" && tt.name != "cache key does not exist" { + if tt.name != "cache is nil" { // check if user was deleted in cache - userData, err := appsession.Cache.Get(cache.UserKey(email)) + userData, err := appsession.Cache.Get(email) assert.NotNil(t, err) assert.Nil(t, userData) } - - if tt.name == "cache key does not exist" { - // check if user was not deleted in cache - userData, err := appsession.Cache.Get(cache.UserKey(email)) - assert.NoError(t, err) - assert.NotNil(t, userData) - } }) } } @@ -266,7 +217,7 @@ func TestGetOTP(t *testing.T) { appsession = &models.AppSession{ Cache: configs.CreateCache(), } - err := appsession.Cache.Set(cache.OTPKey(email, otpv), otpData) + err := appsession.Cache.Set(email+otpv, otpData) assert.NoError(t, err) } else { @@ -326,7 +277,7 @@ func TestSetOTP(t *testing.T) { if tt.name != "cache is nil" { // check if otp was set in cache - otpData, err := appsession.Cache.Get(cache.OTPKey(email, otpv)) + otpData, err := appsession.Cache.Get(email + otpv) assert.NoError(t, err) // unmarshal the otp from the cache @@ -361,13 +312,6 @@ func TestDeleteOTPF(t *testing.T) { OTP: otpv, }, }, - { - name: "cache key does not exist", - expectedOTP: models.OTP{ - Email: "doesnotexist@example.com", - OTP: "012345", - }, - }, } for _, tt := range tests { @@ -381,7 +325,7 @@ func TestDeleteOTPF(t *testing.T) { // add otp to cache otpData, _ := bson.Marshal(otp) - err := appsession.Cache.Set(cache.OTPKey(email, otpv), otpData) + err := appsession.Cache.Set(email+otpv, otpData) assert.NoError(t, err) } else { @@ -390,361 +334,12 @@ func TestDeleteOTPF(t *testing.T) { cache.DeleteOTP(appsession, tt.expectedOTP.Email, tt.expectedOTP.OTP) - if tt.name != "cache is nil" && tt.name != "cache key does not exist" { + if tt.name != "cache is nil" { // check if otp was deleted in cache - otpData, err := appsession.Cache.Get(cache.OTPKey(email, otpv)) + otpData, err := appsession.Cache.Get(email + otpv) assert.NotNil(t, err) assert.Nil(t, otpData) } - - if tt.name == "cache key does not exist" { - // check if otp was not deleted in cache - otpData, err := appsession.Cache.Get(cache.OTPKey(email, otpv)) - assert.NoError(t, err) - assert.NotNil(t, otpData) - } - }) - } -} - -func TestSetBooking(t *testing.T) { - booking := models.Booking{OccupiID: "booking123"} - - tests := []struct { - name string - booking models.Booking - expectedBooking models.Booking - }{ - { - name: "cache is nil", - booking: booking, - expectedBooking: models.Booking{}, - }, - { - name: "successful set booking in cache", - booking: booking, - expectedBooking: booking, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var appsession *models.AppSession - - if tt.name != "cache is nil" { - appsession = &models.AppSession{ - Cache: configs.CreateCache(), - } - } else { - appsession = &models.AppSession{} - } - - cache.SetBooking(appsession, tt.booking) - - if tt.name != "cache is nil" { - // check if booking was set in cache - bookingData, err := appsession.Cache.Get(cache.RoomBookingKey(tt.booking.OccupiID)) - assert.NoError(t, err) - - // unmarshal the booking from the cache - var booking models.Booking - if err := bson.Unmarshal(bookingData, &booking); err != nil { - t.Error("failed to unmarshall", err) - } - - assert.Equal(t, tt.expectedBooking, booking) - } - }) - } -} - -func TestGetBooking(t *testing.T) { - booking := models.Booking{OccupiID: "booking123"} - bookingData, _ := bson.Marshal(booking) - - tests := []struct { - name string - bookingID string - expectedBook models.Booking - expectedErr error - }{ - { - name: "cache is nil", - bookingID: "booking123", - expectedBook: models.Booking{}, - expectedErr: errors.New("cache not found"), - }, - { - name: "cache key does not exist", - bookingID: "booking1234", - expectedBook: models.Booking{}, - expectedErr: errors.New("Entry not found"), - }, - { - name: "successful get booking from cache", - bookingID: "booking123", - expectedBook: booking, - expectedErr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var appsession *models.AppSession - - // add booking to cache - if tt.name != "cache is nil" { - appsession = &models.AppSession{ - Cache: configs.CreateCache(), - } - err := appsession.Cache.Set(cache.RoomBookingKey(booking.OccupiID), bookingData) - - assert.NoError(t, err) - } else { - appsession = &models.AppSession{} - } - - result, err := cache.GetBooking(appsession, tt.bookingID) - - assert.Equal(t, tt.expectedBook, result) - if tt.expectedErr != nil { - assert.EqualError(t, err, tt.expectedErr.Error()) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestDeleteBooking(t *testing.T) { - booking := models.Booking{OccupiID: "booking123"} - - tests := []struct { - name string - bookingID string - expectedBook models.Booking - }{ - { - name: "cache is nil", - bookingID: "booking123", - expectedBook: models.Booking{}, - }, - { - name: "successful delete booking in cache", - bookingID: "booking123", - expectedBook: models.Booking{}, - }, - { - name: "cache key does not exist", - bookingID: "booking1234", - expectedBook: models.Booking{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var appsession *models.AppSession - - if tt.name != "cache is nil" { - appsession = &models.AppSession{ - Cache: configs.CreateCache(), - } - - // add booking to cache - bookingData, _ := bson.Marshal(booking) - err := appsession.Cache.Set(cache.RoomBookingKey(booking.OccupiID), bookingData) - - assert.NoError(t, err) - } else { - appsession = &models.AppSession{} - } - - cache.DeleteBooking(appsession, tt.bookingID) - - if tt.name != "cache is nil" && tt.name != "cache key does not exist" { - // check if booking was deleted in cache - bookingData, err := appsession.Cache.Get(cache.RoomBookingKey(booking.OccupiID)) - assert.NotNil(t, err) - assert.Nil(t, bookingData) - } - - if tt.name == "cache key does not exist" { - // check if booking was not deleted in cache - bookingData, err := appsession.Cache.Get(cache.RoomBookingKey(booking.OccupiID)) - assert.NoError(t, err) - assert.NotNil(t, bookingData) - } - }) - } -} - -func TestSetImage(t *testing.T) { - image := models.Image{ID: "image123"} - - tests := []struct { - name string - image models.Image - expectedImage models.Image - }{ - { - name: "cache is nil", - image: image, - expectedImage: models.Image{}, - }, - { - name: "successful set image in cache", - image: image, - expectedImage: image, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var appsession *models.AppSession - - if tt.name != "cache is nil" { - appsession = &models.AppSession{ - Cache: configs.CreateCache(), - } - } else { - appsession = &models.AppSession{} - } - - cache.SetImage(appsession, tt.image.ID, tt.image) - - if tt.name != "cache is nil" { - // check if image was set in cache - imageData, err := appsession.Cache.Get(cache.ImageKey(tt.image.ID)) - assert.NoError(t, err) - - // unmarshal the image from the cache - var image models.Image - if err := bson.Unmarshal(imageData, &image); err != nil { - t.Error("failed to unmarshall", err) - } - - assert.Equal(t, tt.expectedImage, image) - } - }) - } -} - -func TestGetImage(t *testing.T) { - image := models.Image{ID: "image123"} - imageData, _ := bson.Marshal(image) - - tests := []struct { - name string - imageID string - expectedImg models.Image - expectedErr error - }{ - { - name: "cache is nil", - imageID: "image123", - expectedImg: models.Image{}, - expectedErr: errors.New("cache not found"), - }, - { - name: "cache key does not exist", - imageID: "image1234", - expectedImg: models.Image{}, - expectedErr: errors.New("Entry not found"), - }, - { - name: "successful get image from cache", - imageID: "image123", - expectedImg: image, - expectedErr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var appsession *models.AppSession - - // add image to cache - if tt.name != "cache is nil" { - appsession = &models.AppSession{ - Cache: configs.CreateCache(), - } - err := appsession.Cache.Set(cache.ImageKey(image.ID), imageData) - - assert.NoError(t, err) - } else { - appsession = &models.AppSession{} - } - - result, err := cache.GetImage(appsession, tt.imageID) - - assert.Equal(t, tt.expectedImg, result) - if tt.expectedErr != nil { - assert.EqualError(t, err, tt.expectedErr.Error()) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestDeleteImage(t *testing.T) { - image := models.Image{ID: "image123"} - - tests := []struct { - name string - imageID string - expectedImg models.Image - }{ - { - name: "cache is nil", - imageID: "image123", - expectedImg: models.Image{}, - }, - { - name: "successful delete image in cache", - imageID: "image123", - expectedImg: models.Image{}, - }, - { - name: "cache key does not exist", - imageID: "image1234", - expectedImg: models.Image{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var appsession *models.AppSession - - if tt.name != "cache is nil" { - appsession = &models.AppSession{ - Cache: configs.CreateCache(), - } - - // add image to cache - imageData, _ := bson.Marshal(image) - err := appsession.Cache.Set(cache.ImageKey(image.ID), imageData) - - assert.NoError(t, err) - } else { - appsession = &models.AppSession{} - } - - cache.DeleteImage(appsession, tt.imageID) - - if tt.name != "cache is nil" && tt.name != "cache key does not exist" { - // check if image was deleted in cache - imageData, err := appsession.Cache.Get(cache.ImageKey(image.ID)) - assert.NotNil(t, err) - assert.Nil(t, imageData) - } - - if tt.name == "cache key does not exist" { - // check if image was not deleted in cache - imageData, err := appsession.Cache.Get(cache.ImageKey(image.ID)) - assert.NoError(t, err) - assert.NotNil(t, imageData) - } }) } } diff --git a/occupi-backend/tests/cache_test.go b/occupi-backend/tests/cache_test.go index aeeb8bc0..c9896bbf 100644 --- a/occupi-backend/tests/cache_test.go +++ b/occupi-backend/tests/cache_test.go @@ -10,117 +10,17 @@ import ( "go.mongodb.org/mongo-driver/bson" "github.com/COS301-SE-2024/occupi/occupi-backend/configs" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" ) -func TestSaveBooking_WithCache(t *testing.T) { - // Create database connection and Cache - db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a response writer and context - ctx, _ := gin.CreateTestContext(w) - - // Create a new AppSession with the Cache - appSession := &models.AppSession{ - DB: db, - Cache: Cache, - } - - booking := models.Booking{ - OccupiID: "OCCUPI01", - } - - success, err := database.SaveBooking(ctx, appSession, booking) - assert.True(t, success) - assert.Nil(t, err) - - // Verify the booking is in the Cache - cachedBooking1, err := Cache.Get(cache.RoomBookingKey(booking.OccupiID)) - assert.Nil(t, err) - assert.NotNil(t, cachedBooking1) - - // sleep for 2 * Cache expiry time to ensure the Cache expires - time.Sleep(time.Duration(configs.GetCacheEviction()) * 2 * time.Second) - - // Verify the booking is not in the Cache - cachedBooking2, err := Cache.Get(cache.UserKey(booking.OccupiID)) - assert.NotNil(t, err) - assert.Nil(t, cachedBooking2) -} - -func TestConfirmCheckin_WithCache(t *testing.T) { - // Create database connection and Cache - db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a response writer and context - ctx, _ := gin.CreateTestContext(w) - - // Create a new AppSession with the Cache - appSession := &models.AppSession{ - DB: db, - Cache: Cache, - } - - checkin := models.CheckIn{ - BookingID: "ROOM01", - Creator: "TestConfirmCheckin_WithCache@example.com", - } - - booking := models.Booking{ - OccupiID: checkin.BookingID, - Creator: checkin.Creator, - CheckedIn: false, - } - - collection := db.Database(configs.GetMongoDBName()).Collection("RoomBooking") - _, err := collection.InsertOne(ctx, booking) - - assert.Nil(t, err) - - // marshall and add the booking to cache - bookingData, err := bson.Marshal(booking) - - assert.Nil(t, err) - - err = Cache.Set(cache.RoomBookingKey(booking.OccupiID), bookingData) - - assert.Nil(t, err) - - success, err := database.ConfirmCheckIn(ctx, appSession, checkin) - assert.True(t, success) - assert.Nil(t, err) - - // Verify the booking is in the Cache - cachedBooking1, err := Cache.Get(cache.RoomBookingKey(checkin.BookingID)) - assert.Nil(t, err) - assert.NotNil(t, cachedBooking1) - - // sleep for 2 * Cache expiry time to ensure the Cache expires - time.Sleep(time.Duration(configs.GetCacheEviction()) * 2 * time.Second) - - // Verify the booking is not in the Cache - cachedBooking2, err := Cache.Get(cache.UserKey(checkin.BookingID)) - assert.NotNil(t, err) - assert.Nil(t, cachedBooking2) -} - func TestEmailExistsPerformance(t *testing.T) { email := "TestEmailExistsPerformance@example.com" - // Create database connection and Cache + // Create database connection and cache db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + cache := configs.CreateCache() // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. w := httptest.NewRecorder() @@ -128,12 +28,12 @@ func TestEmailExistsPerformance(t *testing.T) { // Create a response writer and context ctx, _ := gin.CreateTestContext(w) - // Create a new AppSession with the Cache + // Create a new AppSession with the cache appsessionWithCache := &models.AppSession{ DB: db, - Cache: Cache, + Cache: cache, } - // Create a new AppSession without the Cache + // Create a new AppSession without the cache appsessionWithoutCache := &models.AppSession{ DB: db, Cache: nil, @@ -149,31 +49,31 @@ func TestEmailExistsPerformance(t *testing.T) { t.Fatalf("Failed to insert test email into database: %v", err) } - // Test performance with Cache + // Test performance with cache startTime := time.Now() for i := 0; i < 1000; i++ { database.EmailExists(ctx, appsessionWithCache, email) } durationWithCache := time.Since(startTime) - // Test performance without Cache + // Test performance without cache startTime = time.Now() for i := 0; i < 1000; i++ { database.EmailExists(ctx, appsessionWithoutCache, email) } durationWithoutCache := time.Since(startTime) - // Assert that the Cache improves the speed + // Assert that the cache improves the speed if durationWithoutCache <= durationWithCache { - t.Errorf("Cache did not improve performance: duration with Cache %v, duration without Cache %v", durationWithCache, durationWithoutCache) + t.Errorf("Cache did not improve performance: duration with cache %v, duration without cache %v", durationWithCache, durationWithoutCache) } } func TestEmailExists_WithCache(t *testing.T) { email := "TestEmailExists_WithCache@example.com" - // Create database connection and Cache + // Create database connection and cache db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + cache := configs.CreateCache() // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. w := httptest.NewRecorder() @@ -181,10 +81,10 @@ func TestEmailExists_WithCache(t *testing.T) { // Create a response writer and context ctx, _ := gin.CreateTestContext(w) - // Create a new AppSession with the Cache + // Create a new AppSession with the cache appSession := &models.AppSession{ DB: db, - Cache: Cache, + Cache: cache, } // Mock the DB response @@ -204,112 +104,17 @@ func TestEmailExists_WithCache(t *testing.T) { // Verify the response assert.True(t, exists) - // Verify the user is in the Cache - cachedUser, err := Cache.Get(cache.UserKey(email)) + // Verify the user is in the cache + cachedUser, err := cache.Get(email) assert.Nil(t, err) assert.NotNil(t, cachedUser) } -func TestBookingExistsPerformance(t *testing.T) { - id := "OCCUPI0101" - - // Create database connection and Cache - db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a response writer and context - ctx, _ := gin.CreateTestContext(w) - - // Create a new AppSession with the Cache - appsessionWithCache := &models.AppSession{ - DB: db, - Cache: Cache, - } - // Create a new AppSession without the Cache - appsessionWithoutCache := &models.AppSession{ - DB: db, - Cache: nil, - } - - // Mock the DB response - collection := db.Database(configs.GetMongoDBName()).Collection("RoomBooking") - bookingStruct := models.Booking{ - OccupiID: id, - } - _, err := collection.InsertOne(ctx, bookingStruct) - if err != nil { - t.Fatalf("Failed to insert test booking into database: %v", err) - } - - // Test performance with Cache - startTime := time.Now() - for i := 0; i < 1000; i++ { - database.BookingExists(ctx, appsessionWithCache, id) - } - durationWithCache := time.Since(startTime) - - // Test performance without Cache - startTime = time.Now() - for i := 0; i < 1000; i++ { - database.BookingExists(ctx, appsessionWithoutCache, id) - } - durationWithoutCache := time.Since(startTime) - - // Assert that the Cache improves the speed - if durationWithoutCache <= durationWithCache { - t.Errorf("Cache did not improve performance: duration with Cache %v, duration without Cache %v", durationWithCache, durationWithoutCache) - } -} - -func TestBookingExists_WithCache(t *testing.T) { - id := "OCCUPI0101" - // Create database connection and Cache - db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a response writer and context - ctx, _ := gin.CreateTestContext(w) - - // Create a new AppSession with the Cache - appSession := &models.AppSession{ - DB: db, - Cache: Cache, - } - - // Mock the DB response - collection := db.Database(configs.GetMongoDBName()).Collection("RoomBooking") - bookingStruct := models.Booking{ - OccupiID: id, - } - _, err := collection.InsertOne(ctx, bookingStruct) - if err != nil { - t.Fatalf("Failed to insert test booking into database: %v", err) - } - - // call the function to test - exists := database.BookingExists(ctx, appSession, id) - - // Verify the response - assert.True(t, exists) - - // Verify the booking is in the Cache - cachedBooking, err := Cache.Get(cache.RoomBookingKey(id)) - - assert.Nil(t, err) - assert.NotNil(t, cachedBooking) -} - func TestAddUser_WithCache(t *testing.T) { - // Create database connection and Cache + // Create database connection and cache db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + cache := configs.CreateCache() // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. w := httptest.NewRecorder() @@ -317,10 +122,10 @@ func TestAddUser_WithCache(t *testing.T) { // Create a response writer and context ctx, _ := gin.CreateTestContext(w) - // Create a new AppSession with the Cache + // Create a new AppSession with the cache appSession := &models.AppSession{ DB: db, - Cache: Cache, + Cache: cache, } user := models.RegisterUser{ @@ -333,24 +138,24 @@ func TestAddUser_WithCache(t *testing.T) { assert.True(t, success) assert.Nil(t, err) - // Verify the user is in the Cache - cachedUser, err := Cache.Get(cache.UserKey(user.Email)) + // Verify the user is in the cache + cachedUser, err := cache.Get(user.Email) assert.Nil(t, err) assert.NotNil(t, cachedUser) - // sleep for 2 * Cache expiry time to ensure the Cache expires + // sleep for 2 * cache expiry time to ensure the cache expires time.Sleep(time.Duration(configs.GetCacheEviction()) * 2 * time.Second) - // Verify the user is not in the Cache - cachedUser, err = Cache.Get(cache.UserKey(user.Email)) + // Verify the user is not in the cache + cachedUser, err = cache.Get(user.Email) assert.NotNil(t, err) assert.Nil(t, cachedUser) } func TestAddOTP_WithCache(t *testing.T) { - // Create database connection and Cache + // Create database connection and cache db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + cache := configs.CreateCache() // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. w := httptest.NewRecorder() @@ -358,10 +163,10 @@ func TestAddOTP_WithCache(t *testing.T) { // Create a response writer and context ctx, _ := gin.CreateTestContext(w) - // Create a new AppSession with the Cache + // Create a new AppSession with the cache appSession := &models.AppSession{ DB: db, - Cache: Cache, + Cache: cache, } email := "test_withcache@example.com" @@ -371,16 +176,16 @@ func TestAddOTP_WithCache(t *testing.T) { assert.True(t, success) assert.Nil(t, err) - // Verify the otp is in the Cache - cachedUser, err := Cache.Get(cache.OTPKey(email, otp)) + // Verify the otp is in the cache + cachedUser, err := cache.Get(email + otp) assert.Nil(t, err) assert.NotNil(t, cachedUser) - // sleep for 2 * Cache expiry time to ensure the Cache expires + // sleep for 2 * cache expiry time to ensure the cache expires time.Sleep(time.Duration(configs.GetCacheEviction()) * 2 * time.Second) - // Verify the user is not in the Cache - cachedUser, err = Cache.Get(cache.OTPKey(email, otp)) + // Verify the user is not in the cache + cachedUser, err = cache.Get(email + otp) assert.NotNil(t, err) assert.Nil(t, cachedUser) } @@ -389,9 +194,9 @@ func TestOTPExistsPerformance(t *testing.T) { email := "TestOTPExistsPerformance@example.com" otp := "123456" - // Create database connection and Cache + // Create database connection and cache db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + cache := configs.CreateCache() // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. w := httptest.NewRecorder() @@ -399,12 +204,12 @@ func TestOTPExistsPerformance(t *testing.T) { // Create a response writer and context ctx, _ := gin.CreateTestContext(w) - // Create a new AppSession with the Cache + // Create a new AppSession with the cache appsessionWithCache := &models.AppSession{ DB: db, - Cache: Cache, + Cache: cache, } - // Create a new AppSession without the Cache + // Create a new AppSession without the cache appsessionWithoutCache := &models.AppSession{ DB: db, Cache: nil, @@ -422,32 +227,32 @@ func TestOTPExistsPerformance(t *testing.T) { t.Fatalf("Failed to insert test otp into database: %v", err) } - // Test performance with Cache + // Test performance with cache startTime := time.Now() for i := 0; i < 1000; i++ { database.OTPExists(ctx, appsessionWithCache, email, otp) } durationWithCache := time.Since(startTime) - // Test performance without Cache + // Test performance without cache startTime = time.Now() for i := 0; i < 1000; i++ { database.OTPExists(ctx, appsessionWithoutCache, email, otp) } durationWithoutCache := time.Since(startTime) - // Assert that the Cache improves the speed + // Assert that the cache improves the speed if durationWithoutCache <= durationWithCache { - t.Errorf("Cache did not improve performance: duration with Cache %v, duration without Cache %v", durationWithCache, durationWithoutCache) + t.Errorf("Cache did not improve performance: duration with cache %v, duration without cache %v", durationWithCache, durationWithoutCache) } } func TestOTPExists_WithCache(t *testing.T) { email := "TestOTPExists_WithCache@example.com" otp := "123456" - // Create database connection and Cache + // Create database connection and cache db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + cache := configs.CreateCache() // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. w := httptest.NewRecorder() @@ -455,10 +260,10 @@ func TestOTPExists_WithCache(t *testing.T) { // Create a response writer and context ctx, _ := gin.CreateTestContext(w) - // Create a new AppSession with the Cache + // Create a new AppSession with the cache appSession := &models.AppSession{ DB: db, - Cache: Cache, + Cache: cache, } // Mock the DB response @@ -473,8 +278,8 @@ func TestOTPExists_WithCache(t *testing.T) { t.Fatalf("Failed to insert test otp into database: %v", err) } - // Verify the otp is not in the Cache before calling the function - nocachedOTP, err := Cache.Get(cache.OTPKey(email, otp)) + // Verify the otp is not in the cache before calling the function + nocachedOTP, err := cache.Get(email + otp) assert.NotNil(t, err) assert.Nil(t, nocachedOTP) @@ -486,8 +291,8 @@ func TestOTPExists_WithCache(t *testing.T) { assert.True(t, exists) assert.Nil(t, err) - // Verify the otp is in the Cache - cachedUser, err := Cache.Get(cache.OTPKey(email, otp)) + // Verify the user is in the cache + cachedUser, err := cache.Get(email + otp) assert.Nil(t, err) assert.NotNil(t, cachedUser) @@ -496,9 +301,9 @@ func TestOTPExists_WithCache(t *testing.T) { func TestDeleteOTP_withCache(t *testing.T) { email := "TestDeleteOTP_withCache@example.com" otp := "123456" - // Create database connection and Cache + // Create database connection and cache db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + cache := configs.CreateCache() // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. w := httptest.NewRecorder() @@ -506,10 +311,10 @@ func TestDeleteOTP_withCache(t *testing.T) { // Create a response writer and context ctx, _ := gin.CreateTestContext(w) - // Create a new AppSession with the Cache + // Create a new AppSession with the cache appSession := &models.AppSession{ DB: db, - Cache: Cache, + Cache: cache, } // Mock the DB response @@ -524,17 +329,17 @@ func TestDeleteOTP_withCache(t *testing.T) { t.Fatalf("Failed to insert test otp into database: %v", err) } - // add otp to Cache + // add otp to cache if otpData, err := bson.Marshal(otpStruct); err != nil { t.Fatal(err) } else { - if err := Cache.Set(cache.OTPKey(email, otp), otpData); err != nil { + if err := cache.Set(email+otp, otpData); err != nil { t.Fatal(err) } } - // Verify the otp is in the Cache before calling the function - nocachedOTP, err := Cache.Get(cache.OTPKey(email, otp)) + // Verify the otp is in the cache before calling the function + nocachedOTP, err := cache.Get(email + otp) assert.Nil(t, err) assert.NotNil(t, nocachedOTP) @@ -546,8 +351,8 @@ func TestDeleteOTP_withCache(t *testing.T) { assert.True(t, success) assert.Nil(t, err) - // Verify the otp is not in the Cache - cachedUser, err := Cache.Get(cache.OTPKey(email, otp)) + // Verify the otp is not in the cache + cachedUser, err := cache.Get(email + otp) assert.NotNil(t, err) assert.Nil(t, cachedUser) @@ -556,9 +361,9 @@ func TestDeleteOTP_withCache(t *testing.T) { func TestGetPasswordPerformance(t *testing.T) { email := "TestGetPasswordPerformance@example.com" password := "password" - // Create database connection and Cache + // Create database connection and cache db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + cache := configs.CreateCache() // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. w := httptest.NewRecorder() @@ -566,12 +371,12 @@ func TestGetPasswordPerformance(t *testing.T) { // Create a response writer and context ctx, _ := gin.CreateTestContext(w) - // Create a new AppSession with the Cache + // Create a new AppSession with the cache appsessionWithCache := &models.AppSession{ DB: db, - Cache: Cache, + Cache: cache, } - // Create a new AppSession without the Cache + // Create a new AppSession without the cache appsessionWithoutCache := &models.AppSession{ DB: db, Cache: nil, @@ -589,32 +394,32 @@ func TestGetPasswordPerformance(t *testing.T) { t.Fatalf("Failed to insert test user into database: %v", err) } - // Test performance with Cache + // Test performance with cache startTime := time.Now() for i := 0; i < 1000; i++ { database.GetPassword(ctx, appsessionWithCache, email) } durationWithCache := time.Since(startTime) - // Test performance without Cache + // Test performance without cache startTime = time.Now() for i := 0; i < 1000; i++ { database.GetPassword(ctx, appsessionWithoutCache, email) } durationWithoutCache := time.Since(startTime) - // Assert that the Cache improves the speed + // Assert that the cache improves the speed if durationWithoutCache <= durationWithCache { - t.Errorf("Cache did not improve performance: duration with Cache %v, duration without Cache %v", durationWithCache, durationWithoutCache) + t.Errorf("Cache did not improve performance: duration with cache %v, duration without cache %v", durationWithCache, durationWithoutCache) } } func TestGetPassword_withCache(t *testing.T) { email := "TestGetPassword_withCache@example.com" password := "password" - // Create database connection and Cache + // Create database connection and cache db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + cache := configs.CreateCache() // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. w := httptest.NewRecorder() @@ -622,10 +427,10 @@ func TestGetPassword_withCache(t *testing.T) { // Create a response writer and context ctx, _ := gin.CreateTestContext(w) - // Create a new AppSession with the Cache + // Create a new AppSession with the cache appSession := &models.AppSession{ DB: db, - Cache: Cache, + Cache: cache, } // Mock the DB response @@ -640,8 +445,8 @@ func TestGetPassword_withCache(t *testing.T) { t.Fatalf("Failed to insert test user into database: %v", err) } - // Verify the user is not in the Cache before calling the function - nocachedUser, err := Cache.Get(cache.UserKey(email)) + // Verify the user is not in the cache before calling the function + nocachedUser, err := cache.Get(email) assert.NotNil(t, err) assert.Nil(t, nocachedUser) @@ -653,8 +458,8 @@ func TestGetPassword_withCache(t *testing.T) { assert.Equal(t, password, passwordv) assert.Nil(t, err) - // Verify the user is in the Cache - cachedUser, err := Cache.Get(cache.UserKey(email)) + // Verify the user is in the cache + cachedUser, err := cache.Get(email) assert.Nil(t, err) assert.NotNil(t, cachedUser) @@ -662,9 +467,9 @@ func TestGetPassword_withCache(t *testing.T) { func TestCheckIfUserIsAdminPerformance(t *testing.T) { email := "TestCheckIfUserIsAdminPerformance@example.com" - // Create database connection and Cache + // Create database connection and cache db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + cache := configs.CreateCache() // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. w := httptest.NewRecorder() @@ -672,12 +477,12 @@ func TestCheckIfUserIsAdminPerformance(t *testing.T) { // Create a response writer and context ctx, _ := gin.CreateTestContext(w) - // Create a new AppSession with the Cache + // Create a new AppSession with the cache appsessionWithCache := &models.AppSession{ DB: db, - Cache: Cache, + Cache: cache, } - // Create a new AppSession without the Cache + // Create a new AppSession without the cache appsessionWithoutCache := &models.AppSession{ DB: db, Cache: nil, @@ -695,7 +500,7 @@ func TestCheckIfUserIsAdminPerformance(t *testing.T) { t.Fatalf("Failed to insert test user into database: %v", err) } - // Test performance with Cache + // Test performance with cache startTime := time.Now() for i := 0; i < 1000; i++ { database.CheckIfUserIsAdmin(ctx, appsessionWithCache, email) @@ -703,7 +508,7 @@ func TestCheckIfUserIsAdminPerformance(t *testing.T) { durationWithCache := time.Since(startTime) - // Test performance without Cache + // Test performance without cache startTime = time.Now() for i := 0; i < 1000; i++ { database.CheckIfUserIsAdmin(ctx, appsessionWithoutCache, email) @@ -711,18 +516,18 @@ func TestCheckIfUserIsAdminPerformance(t *testing.T) { durationWithoutCache := time.Since(startTime) - // Assert that the Cache improves the speed + // Assert that the cache improves the speed if durationWithoutCache <= durationWithCache { - t.Errorf("Cache did not improve performance: duration with Cache %v, duration without Cache %v", durationWithCache, durationWithoutCache) + t.Errorf("Cache did not improve performance: duration with cache %v, duration without cache %v", durationWithCache, durationWithoutCache) } } func TestCheckIfUserIsAdmin_WithCache(t *testing.T) { email1 := "TestCheckIfUserIsAdmin_WithCache1@example.com" email2 := "TestCheckIfUserIsAdmin_WithCache2@example.com" - // Create database connection and Cache + // Create database connection and cache db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + cache := configs.CreateCache() // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. w := httptest.NewRecorder() @@ -730,10 +535,10 @@ func TestCheckIfUserIsAdmin_WithCache(t *testing.T) { // Create a response writer and context ctx, _ := gin.CreateTestContext(w) - // Create a new AppSession with the Cache + // Create a new AppSession with the cache appSession := &models.AppSession{ DB: db, - Cache: Cache, + Cache: cache, } // Mock the DB response @@ -758,13 +563,13 @@ func TestCheckIfUserIsAdmin_WithCache(t *testing.T) { t.Fatalf("Failed to insert test user into database: %v", err) } - // Verify the user is not in the Cache before calling the function - nocachedUser1, err := Cache.Get(cache.UserKey(email1)) + // Verify the user is not in the cache before calling the function + nocachedUser1, err := cache.Get(email1) assert.NotNil(t, err) assert.Nil(t, nocachedUser1) - nocachedUser2, err := Cache.Get(cache.UserKey(email2)) + nocachedUser2, err := cache.Get(email2) assert.NotNil(t, err) assert.Nil(t, nocachedUser2) @@ -776,8 +581,8 @@ func TestCheckIfUserIsAdmin_WithCache(t *testing.T) { assert.True(t, isAdmin1) assert.Nil(t, err) - // Verify the user is in the Cache - cachedUser1, err := Cache.Get(cache.UserKey(email1)) + // Verify the user is in the cache + cachedUser1, err := cache.Get(email1) assert.Nil(t, err) assert.NotNil(t, cachedUser1) @@ -789,8 +594,8 @@ func TestCheckIfUserIsAdmin_WithCache(t *testing.T) { assert.False(t, isAdmin2) assert.Nil(t, err) - // Verify the user is in the Cache - cachedUser2, err := Cache.Get(cache.UserKey(email2)) + // Verify the user is in the cache + cachedUser2, err := cache.Get(email2) assert.Nil(t, err) assert.NotNil(t, cachedUser2) @@ -798,9 +603,9 @@ func TestCheckIfUserIsAdmin_WithCache(t *testing.T) { func TestCheckIfUserIsLoggingInFromKnownLocationPerformance(t *testing.T) { email := "TestCheckIfUserIsLoggingInFromKnownLocationPerformance@example.com" - // Create database connection and Cache + // Create database connection and cache db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + cache := configs.CreateCache() // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. w := httptest.NewRecorder() @@ -808,12 +613,12 @@ func TestCheckIfUserIsLoggingInFromKnownLocationPerformance(t *testing.T) { // Create a response writer and context ctx, _ := gin.CreateTestContext(w) - // Create a new AppSession with the Cache + // Create a new AppSession with the cache appsessionWithCache := &models.AppSession{ DB: db, - Cache: Cache, + Cache: cache, } - // Create a new AppSession without the Cache + // Create a new AppSession without the cache appsessionWithoutCache := &models.AppSession{ DB: db, Cache: nil, @@ -837,7 +642,7 @@ func TestCheckIfUserIsLoggingInFromKnownLocationPerformance(t *testing.T) { t.Fatalf("Failed to insert test user into database: %v", err) } - // Test performance with Cache + // Test performance with cache startTime := time.Now() for i := 0; i < 1000; i++ { database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsessionWithCache, email, "8.8.8.8") @@ -845,7 +650,7 @@ func TestCheckIfUserIsLoggingInFromKnownLocationPerformance(t *testing.T) { durationWithCache := time.Since(startTime) - // Test performance without Cache + // Test performance without cache startTime = time.Now() for i := 0; i < 1000; i++ { database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsessionWithoutCache, email, "8.8.8.8") @@ -853,17 +658,17 @@ func TestCheckIfUserIsLoggingInFromKnownLocationPerformance(t *testing.T) { durationWithoutCache := time.Since(startTime) - // Assert that the Cache improves the speed + // Assert that the cache improves the speed if durationWithoutCache <= durationWithCache { - t.Errorf("Cache did not improve performance: duration with Cache %v, duration without Cache %v", durationWithCache, durationWithoutCache) + t.Errorf("Cache did not improve performance: duration with cache %v, duration without cache %v", durationWithCache, durationWithoutCache) } } func TestCheckIfUserIsLoggingInFromKnownLocation_withCache(t *testing.T) { email := "TestCheckIfUserIsLoggingInFromKnownLocation_withCache@example.com" - // Create database connection and Cache + // Create database connection and cache db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + cache := configs.CreateCache() // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. w := httptest.NewRecorder() @@ -871,10 +676,10 @@ func TestCheckIfUserIsLoggingInFromKnownLocation_withCache(t *testing.T) { // Create a response writer and context ctx, _ := gin.CreateTestContext(w) - // Create a new AppSession with the Cache + // Create a new AppSession with the cache appSession := &models.AppSession{ DB: db, - Cache: Cache, + Cache: cache, } // Mock the DB response @@ -895,8 +700,8 @@ func TestCheckIfUserIsLoggingInFromKnownLocation_withCache(t *testing.T) { t.Fatalf("Failed to insert test user into database: %v", err) } - // Verify the user is not in the Cache before calling the function - nocachedUser, err := Cache.Get(cache.UserKey(email)) + // Verify the user is not in the cache before calling the function + nocachedUser, err := cache.Get(email) assert.NotNil(t, err) assert.Nil(t, nocachedUser) @@ -909,296 +714,8 @@ func TestCheckIfUserIsLoggingInFromKnownLocation_withCache(t *testing.T) { assert.Nil(t, err) assert.Nil(t, info) - // Verify the user is in the Cache - cachedUser, err := Cache.Get(cache.UserKey(email)) - - assert.Nil(t, err) - assert.NotNil(t, cachedUser) -} - -func TestGetUserDetailsPerformance(t *testing.T) { - userStruct := models.User{ - OccupiID: "123456", - Password: "hashedpassword123", - Email: "test@example.com", - Role: constants.Admin, - OnSite: true, - IsVerified: true, - NextVerificationDate: time.Now(), // this will be updated once the email is verified - TwoFAEnabled: false, - KnownLocations: []models.Location{ - { - City: "Cape Town", - Region: "Western Cape", - Country: "South Africa", - }, - }, - Details: models.Details{ - ImageID: "", - Name: "Michael", - DOB: time.Now(), - Gender: "Male", - Pronouns: "He/Him", - }, - Notifications: models.Notifications{ - Invites: true, - BookingReminder: true, - }, - Security: models.Security{ - MFA: false, - Biometrics: false, - ForceLogout: false, - }, - Status: "5", - Position: "Chief Executive Engineer", - DepartmentNo: "01", - ExpoPushToken: "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]", - } - - // Create database connection and Cache - db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a response writer and context - ctx, _ := gin.CreateTestContext(w) - - // Create a new AppSession with the Cache - appsessionWithCache := &models.AppSession{ - DB: db, - Cache: Cache, - } - // Create a new AppSession without the Cache - appsessionWithoutCache := &models.AppSession{ - DB: db, - Cache: nil, - } - - // Mock the DB response - collection := db.Database(configs.GetMongoDBName()).Collection("Users") - - _, err := collection.InsertOne(ctx, userStruct) - if err != nil { - t.Fatalf("Failed to insert test user into database: %v", err) - } - - // Test performance with Cache - startTime := time.Now() - for i := 0; i < 1000; i++ { - database.GetUserDetails(ctx, appsessionWithCache, userStruct.Email) - } - durationWithCache := time.Since(startTime) - - // Test performance without Cache - startTime = time.Now() - for i := 0; i < 1000; i++ { - database.GetUserDetails(ctx, appsessionWithoutCache, userStruct.Email) - } - durationWithoutCache := time.Since(startTime) - - // Assert that the Cache improves the speed - if durationWithoutCache <= durationWithCache { - t.Errorf("Cache did not improve performance: duration with Cache %v, duration without Cache %v", durationWithCache, durationWithoutCache) - } -} - -func TestGetUserDetails_withCache(t *testing.T) { - userStruct := models.User{ - OccupiID: "123456", - Password: "hashedpassword123", - Email: "test@example.com", - Role: constants.Admin, - OnSite: true, - IsVerified: true, - NextVerificationDate: time.Now(), // this will be updated once the email is verified - TwoFAEnabled: false, - KnownLocations: []models.Location{ - { - City: "Cape Town", - Region: "Western Cape", - Country: "South Africa", - }, - }, - Details: models.Details{ - ImageID: "", - Name: "Michael", - DOB: time.Now(), - Gender: "Male", - Pronouns: "He/Him", - }, - Notifications: models.Notifications{ - Invites: true, - BookingReminder: true, - }, - Security: models.Security{ - MFA: false, - Biometrics: false, - ForceLogout: false, - }, - Status: "5", - Position: "Chief Executive Engineer", - DepartmentNo: "01", - ExpoPushToken: "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]", - } - // Create database connection and Cache - db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a response writer and context - ctx, _ := gin.CreateTestContext(w) - - // Create a new AppSession with the Cache - appSession := &models.AppSession{ - DB: db, - Cache: Cache, - } - - // Mock the DB response - collection := db.Database(configs.GetMongoDBName()).Collection("Users") - - _, err := collection.InsertOne(ctx, userStruct) - if err != nil { - t.Fatalf("Failed to insert test user into database: %v", err) - } - - // Verify the user is not in the Cache before calling the function - nocachedUser, err := Cache.Get(cache.UserKey(userStruct.Email)) - - assert.NotNil(t, err) - assert.Nil(t, nocachedUser) - - // call the function to test - user, err := database.GetUserDetails(ctx, appSession, userStruct.Email) - - // Verify the response - assert.Equal(t, userStruct.Email, user.Email) - assert.Equal(t, userStruct.Details.Name, user.Name) - assert.Equal(t, userStruct.Details.Gender, user.Gender) - assert.Equal(t, userStruct.Details.Pronouns, user.Pronouns) - assert.Equal(t, userStruct.Details.ContactNo, user.Number) - assert.Nil(t, err) - - // Verify the user is in the Cache - cachedUser, err := Cache.Get(cache.UserKey(userStruct.Email)) - - assert.Nil(t, err) - assert.NotNil(t, cachedUser) -} - -func TestGetSecuritySettingsPerformance(t *testing.T) { - userStruct := models.User{ - Email: "test@example.com", - Security: models.Security{ - MFA: false, - Biometrics: false, - ForceLogout: false, - }, - } - - // Create database connection and Cache - db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a response writer and context - ctx, _ := gin.CreateTestContext(w) - - // Create a new AppSession with the Cache - appsessionWithCache := &models.AppSession{ - DB: db, - Cache: Cache, - } - // Create a new AppSession without the Cache - appsessionWithoutCache := &models.AppSession{ - DB: db, - Cache: nil, - } - - // Mock the DB response - collection := db.Database(configs.GetMongoDBName()).Collection("Users") - - _, err := collection.InsertOne(ctx, userStruct) - if err != nil { - t.Fatalf("Failed to insert test user into database: %v", err) - } - - // Test performance with Cache - startTime := time.Now() - for i := 0; i < 1000; i++ { - database.GetSecuritySettings(ctx, appsessionWithCache, userStruct.Email) - } - durationWithCache := time.Since(startTime) - - // Test performance without Cache - startTime = time.Now() - for i := 0; i < 1000; i++ { - database.GetSecuritySettings(ctx, appsessionWithoutCache, userStruct.Email) - } - durationWithoutCache := time.Since(startTime) - - // Assert that the Cache improves the speed - if durationWithoutCache <= durationWithCache { - t.Errorf("Cache did not improve performance: duration with Cache %v, duration without Cache %v", durationWithCache, durationWithoutCache) - } -} - -func TestGetSecuritySettings_withCache(t *testing.T) { - userStruct := models.User{ - Email: "test@example.com", - Security: models.Security{ - MFA: false, - Biometrics: false, - ForceLogout: false, - }, - } - // Create database connection and Cache - db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a response writer and context - ctx, _ := gin.CreateTestContext(w) - - // Create a new AppSession with the Cache - appSession := &models.AppSession{ - DB: db, - Cache: Cache, - } - - // Mock the DB response - collection := db.Database(configs.GetMongoDBName()).Collection("Users") - - _, err := collection.InsertOne(ctx, userStruct) - if err != nil { - t.Fatalf("Failed to insert test user into database: %v", err) - } - - // Verify the user is not in the Cache before calling the function - nocachedUser, err := Cache.Get(cache.UserKey(userStruct.Email)) - - assert.NotNil(t, err) - assert.Nil(t, nocachedUser) - - // call the function to test - user, err := database.GetSecuritySettings(ctx, appSession, userStruct.Email) - - // Verify the response - assert.Equal(t, userStruct.Email, user.Email) - assert.Equal(t, "off", user.Mfa) - assert.Equal(t, "off", user.ForceLogout) - assert.Nil(t, err) - - // Verify the user is in the Cache - cachedUser, err := Cache.Get(cache.UserKey(userStruct.Email)) + // Verify the user is in the cache + cachedUser, err := cache.Get(email) assert.Nil(t, err) assert.NotNil(t, cachedUser) diff --git a/occupi-backend/tests/database_test.go b/occupi-backend/tests/database_test.go index e17f30ed..9622819b 100644 --- a/occupi-backend/tests/database_test.go +++ b/occupi-backend/tests/database_test.go @@ -22,7 +22,6 @@ import ( "github.com/COS301-SE-2024/occupi/occupi-backend/configs" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/cache" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" @@ -131,212 +130,6 @@ func TestGetAllData(t *testing.T) { }) } -func TestSaveBooking(t *testing.T) { - // Setup mock MongoDB instance - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - gin.SetMode(configs.GetGinRunMode()) - - // Create a new HTTP request with the POST method. - req, _ := http.NewRequest("POST", "/", nil) - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a new context with the Request and ResponseWriter. - ctx, _ := gin.CreateTestContext(w) - ctx.Request = req - - // Optionally, set any values in the context. - ctx.Set("test", "test") - - booking := models.Booking{ - OccupiID: "OCCUPI01", - } - - mt.Run("Nil database", func(mt *mtest.T) { - // Call the function under test - appsession := &models.AppSession{} - success, err := database.SaveBooking(ctx, appsession, booking) - - // Validate the result - assert.Error(t, err) - assert.False(t, success) - }) - - mt.Run("Add room successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - success, err := database.SaveBooking(ctx, appsession, booking) - - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - }) - - mt.Run("Add room successfully to Cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - - // Call the function under test - success, err := database.SaveBooking(ctx, appsession, booking) - - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - - // Verify the room was added to the Cache - roomv, err := Cache.Get(cache.RoomBookingKey(booking.OccupiID)) - - assert.Nil(t, err) - assert.NotNil(t, roomv) - }) - - mt.Run("InsertOne error", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "duplicate key error", - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - success, err := database.SaveBooking(ctx, appsession, booking) - - // Validate the result - assert.Error(t, err) - assert.False(t, success) - }) -} - -func TestConfirmCheckIn(t *testing.T) { - // Setup mock MongoDB instance - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - gin.SetMode(configs.GetGinRunMode()) - - // Create a new HTTP request with the POST method. - req, _ := http.NewRequest("POST", "/", nil) - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a new context with the Request and ResponseWriter. - ctx, _ := gin.CreateTestContext(w) - ctx.Request = req - - // Optionally, set any values in the context. - ctx.Set("test", "test") - - checkin := models.CheckIn{ - BookingID: "ROOM01", - Creator: "test@example.com", - } - - mt.Run("Nil database", func(mt *mtest.T) { - // Call the function under test - appsession := &models.AppSession{} - success, err := database.ConfirmCheckIn(ctx, appsession, checkin) - - // Validate the result - assert.Error(t, err) - assert.False(t, success) - }) - - mt.Run("Check in successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch, bson.D{ - {Key: "email", Value: checkin.Creator}, - {Key: "roomId", Value: checkin.BookingID}, - {Key: "checkedIn", Value: false}, - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - success, err := database.ConfirmCheckIn(ctx, appsession, checkin) - - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - }) - - mt.Run("Check in successfully in Cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - - booking := models.Booking{ - OccupiID: checkin.BookingID, - Creator: checkin.Creator, - CheckedIn: false, - } - - // marshall and add the booking to cache - bookingData, err := bson.Marshal(booking) - - assert.Nil(t, err) - - err = Cache.Set(cache.RoomBookingKey(booking.OccupiID), bookingData) - - assert.Nil(t, err) - - // Call the function under test - success, err := database.ConfirmCheckIn(ctx, appsession, checkin) - - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - - // Verify the room was added to the Cache - bookingv, err := Cache.Get(cache.RoomBookingKey(booking.OccupiID)) - - assert.Nil(t, err) - assert.NotNil(t, bookingv) - - // unmarshall - var booking2 models.Booking - err = bson.Unmarshal(bookingv, &booking2) - - assert.Nil(t, err) - - assert.True(t, booking2.CheckedIn) - }) - - mt.Run("InsertOne error", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "duplicate key error", - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - success, err := database.ConfirmCheckIn(ctx, appsession, checkin) - - // Validate the result - assert.Error(t, err) - assert.False(t, success) - }) -} - func TestEmailExists(t *testing.T) { // Setup mock MongoDB instance mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) @@ -382,16 +175,16 @@ func TestEmailExists(t *testing.T) { assert.True(t, exists) }) - mt.Run("Email exists adding to Cache", func(mt *mtest.T) { + mt.Run("Email exists adding to cache", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ {Key: "email", Value: email}, })) - Cache := configs.CreateCache() + cache := configs.CreateCache() appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } // Call the function under test @@ -400,8 +193,8 @@ func TestEmailExists(t *testing.T) { // Validate the result assert.True(t, exists) - // Check if the email exists in the Cache - email, err := Cache.Get(cache.UserKey(email)) + // Check if the email exists in the cache + email, err := cache.Get(email) assert.NoError(t, err) assert.NotNil(t, email) }) @@ -436,105 +229,6 @@ func TestEmailExists(t *testing.T) { }) } -func TestBookingExists(t *testing.T) { - // Setup mock MongoDB instance - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - gin.SetMode(configs.GetGinRunMode()) - - // Create a new HTTP request with the POST method. - req, _ := http.NewRequest("POST", "/", nil) - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a new context with the Request and ResponseWriter. - ctx, _ := gin.CreateTestContext(w) - ctx.Request = req - - // Optionally, set any values in the context. - ctx.Set("test", "test") - - id := "OCCUPI0101" - - mt.Run("Nil database", func(mt *mtest.T) { - // Call the function under test - appsession := &models.AppSession{} - exists := database.BookingExists(ctx, appsession, id) - - // Validate the result - assert.False(t, exists) - }) - - mt.Run("Booking exists", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch, bson.D{ - {Key: "occupiId", Value: id}, - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - exists := database.BookingExists(ctx, appsession, id) - - // Validate the result - assert.True(t, exists) - }) - - mt.Run("Email exists adding to Cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch, bson.D{ - {Key: "occupiId", Value: id}, - })) - - Cache := configs.CreateCache() - - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - - // Call the function under test - exists := database.BookingExists(ctx, appsession, id) - - // Validate the result - assert.True(t, exists) - - // Check if the email exists in the Cache - booking, err := Cache.Get(cache.RoomBookingKey(id)) - assert.NoError(t, err) - assert.NotNil(t, booking) - }) - - mt.Run("Email does not exist", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch)) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - exists := database.BookingExists(ctx, appsession, id) - - // Validate the result - assert.False(t, exists) - }) - - mt.Run("Handle find error", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 1, - Message: "find error", - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - exists := database.BookingExists(ctx, appsession, id) - - // Validate the result - assert.False(t, exists) - }) -} - func TestAddUser(t *testing.T) { // Setup mock MongoDB instance mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) @@ -584,14 +278,14 @@ func TestAddUser(t *testing.T) { assert.True(t, success) }) - mt.Run("Add user successfully to Cache", func(mt *mtest.T) { + mt.Run("Add user successfully to cache", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) - Cache := configs.CreateCache() + cache := configs.CreateCache() appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } // Call the function under test @@ -601,8 +295,8 @@ func TestAddUser(t *testing.T) { assert.NoError(t, err) assert.True(t, success) - // Verify the user was added to the Cache - user, err := Cache.Get(cache.UserKey(user.Email)) + // Verify the user was added to the cache + user, err := cache.Get(user.Email) assert.Nil(t, err) assert.NotNil(t, user) @@ -678,10 +372,10 @@ func TestOTPExists(t *testing.T) { assert.True(t, exists) }) - mt.Run("OTP exists and is valid in Cache", func(mt *mtest.T) { + mt.Run("OTP exists and is valid in cache", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) - Cache := configs.CreateCache() + cache := configs.CreateCache() otpStruct := models.OTP{ Email: email, @@ -689,24 +383,24 @@ func TestOTPExists(t *testing.T) { ExpireWhen: validOTP, } - // add otp to Cache + // add otp to cache if otpData, err := bson.Marshal(otpStruct); err != nil { t.Fatal(err) } else { - if err := Cache.Set(cache.OTPKey(email, otp), otpData); err != nil { + if err := cache.Set(email+otp, otpData); err != nil { t.Fatal(err) } } - // Assert that the otp is in the Cache - otpA, err := Cache.Get(cache.OTPKey(email, otp)) + // Assert that the otp is in the cache + otpA, err := cache.Get(email + otp) assert.Nil(t, err) assert.NotNil(t, otpA) appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } // Call the function under test @@ -735,10 +429,10 @@ func TestOTPExists(t *testing.T) { assert.False(t, exists) }) - mt.Run("OTP exists but is expired in Cache", func(mt *mtest.T) { + mt.Run("OTP exists but is expired in cache", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) - Cache := configs.CreateCache() + cache := configs.CreateCache() otpStruct := models.OTP{ Email: email, @@ -746,24 +440,24 @@ func TestOTPExists(t *testing.T) { ExpireWhen: expiredOTP, } - // add otp to Cache + // add otp to cache if otpData, err := bson.Marshal(otpStruct); err != nil { t.Fatal(err) } else { - if err := Cache.Set(cache.OTPKey(email, otp), otpData); err != nil { + if err := cache.Set(email+otp, otpData); err != nil { t.Fatal(err) } } - // Assert that the otp is in the Cache - otpA, err := Cache.Get(cache.OTPKey(email, otp)) + // Assert that the otp is in the cache + otpA, err := cache.Get(email + otp) assert.Nil(t, err) assert.NotNil(t, otpA) appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } // Call the function under test @@ -854,14 +548,14 @@ func TestAddOTP(t *testing.T) { // Verify the inserted document }) - mt.Run("Add OTP successfully to Cache", func(mt *mtest.T) { + mt.Run("Add OTP successfully to cache", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) - Cache := configs.CreateCache() + cache := configs.CreateCache() appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } // Call the function under test @@ -871,8 +565,8 @@ func TestAddOTP(t *testing.T) { assert.NoError(t, err) assert.True(t, success) - // Verify the otp was added to the Cache - otp, err := Cache.Get(cache.OTPKey(email, otp)) + // Verify the otp was added to the cache + otp, err := cache.Get(email + otp) assert.Nil(t, err) assert.NotNil(t, otp) @@ -1013,18 +707,18 @@ func TestVerifyUser(t *testing.T) { // Verify the update }) - mt.Run("Verify user successfully and user is not in Cache", func(mt *mtest.T) { + mt.Run("Verify user successfully and user is not in cache", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".OTPS", mtest.FirstBatch, bson.D{ {Key: "email", Value: email}, {Key: "isVerified", Value: false}, {Key: "nextVerificationDate", Value: time.Now().Add(-1 * time.Hour)}, })) - Cache := configs.CreateCache() + cache := configs.CreateCache() appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } // Call the function under test @@ -1037,14 +731,14 @@ func TestVerifyUser(t *testing.T) { // Verify the update }) - mt.Run("Verify user successfully and user is in Cache", func(mt *mtest.T) { + mt.Run("Verify user successfully and user is in cache", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".OTPS", mtest.FirstBatch, bson.D{ {Key: "email", Value: email}, {Key: "isVerified", Value: false}, {Key: "nextVerificationDate", Value: time.Now().Add(-1 * time.Hour)}, })) - Cache := configs.CreateCache() + cache := configs.CreateCache() userStruct := models.User{ Email: email, @@ -1052,24 +746,24 @@ func TestVerifyUser(t *testing.T) { NextVerificationDate: time.Now().Add(-1 * time.Hour), } - // add user to Cache + // add user to cache if userData, err := bson.Marshal(userStruct); err != nil { t.Fatal(err) } else { - if err := Cache.Set(cache.UserKey(email), userData); err != nil { + if err := cache.Set(email, userData); err != nil { t.Fatal(err) } } - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(email)) + // Assert that the user is in the cache + userA, err := cache.Get(email) assert.Nil(t, err) assert.NotNil(t, userA) appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } // Call the function under test @@ -1079,8 +773,8 @@ func TestVerifyUser(t *testing.T) { assert.NoError(t, err) assert.True(t, success) - // Verify the update in Cache - user, err := Cache.Get(cache.UserKey(email)) + // Verify the update in cache + user, err := cache.Get(email) assert.Nil(t, err) assert.NotNil(t, user) @@ -1166,34 +860,34 @@ func TestGetPassword(t *testing.T) { assert.Equal(t, password, pass) }) - mt.Run("Get password successfully from Cache", func(mt *mtest.T) { + mt.Run("Get password successfully from cache", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) - Cache := configs.CreateCache() + cache := configs.CreateCache() userStruct := models.User{ Email: email, Password: password, } - // Add password to Cache + // Add password to cache if passData, err := bson.Marshal(userStruct); err != nil { t.Fatal(err) } else { - if err := Cache.Set(cache.UserKey(email), passData); err != nil { + if err := cache.Set(email, passData); err != nil { t.Fatal(err) } } - // Assert that the password is in the Cache - pass, err := Cache.Get(cache.UserKey(email)) + // Assert that the password is in the cache + pass, err := cache.Get(email) assert.Nil(t, err) assert.NotNil(t, pass) appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } // Call the function under test @@ -1222,7 +916,7 @@ func TestGetPassword(t *testing.T) { }) } -func TestCheckIfNextVerificationDateIsDue(t *testing.T) { +func TestCheckIfUserIsVerified(t *testing.T) { // Setup mock MongoDB instance mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) @@ -1241,285 +935,41 @@ func TestCheckIfNextVerificationDateIsDue(t *testing.T) { // Optionally, set any values in the context. ctx.Set("test", "test") - email1 := "test1@example.com" - email2 := "test2@example.com" - - dueDate := time.Now().Add(-1 * time.Hour) - notDueDate := time.Now().Add(1 * time.Hour) + email := "test@example.com" mt.Run("Nil database", func(mt *mtest.T) { // Call the function under test appsession := &models.AppSession{} - isDue, err := database.CheckIfNextVerificationDateIsDue(ctx, appsession, email1) + isVerified, err := database.CheckIfUserIsVerified(ctx, appsession, email) // Validate the result assert.Error(t, err) - assert.False(t, isDue) + assert.False(t, isVerified) }) - mt.Run("Verification date is not due", func(mt *mtest.T) { + mt.Run("User is verified", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email1}, + {Key: "email", Value: email}, {Key: "isVerified", Value: true}, - {Key: "nextVerificationDate", Value: notDueDate}, })) // Call the function under test appsession := &models.AppSession{ DB: mt.Client, } - isDue, err := database.CheckIfNextVerificationDateIsDue(ctx, appsession, email1) + isVerified, err := database.CheckIfUserIsVerified(ctx, appsession, email) // Validate the result assert.NoError(t, err) - assert.False(t, isDue) + assert.True(t, isVerified) }) - mt.Run("Verification date is due", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(2, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email2}, - {Key: "isVerified", Value: true}, - {Key: "nextVerificationDate", Value: dueDate}, + mt.Run("User is not verified", func(mt *mtest.T) { + mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ + {Key: "email", Value: email}, + {Key: "isVerified", Value: false}, })) - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - isDue, err := database.CheckIfNextVerificationDateIsDue(ctx, appsession, email2) - - // Validate the result - assert.NoError(t, err) - assert.True(t, isDue) - }) - - mt.Run("Verification date is not due in cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - userStruct := models.User{ - Email: email1, - IsVerified: false, - NextVerificationDate: notDueDate, - } - - // add user to Cache - if userData, err := bson.Marshal(userStruct); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(email1), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(email1)) - - assert.Nil(t, err) - assert.NotNil(t, userA) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - isDue, err := database.CheckIfNextVerificationDateIsDue(ctx, appsession, email1) - - // Validate the result - assert.NoError(t, err) - assert.False(t, isDue) - }) - - mt.Run("Verification date is due in cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - userStruct := models.User{ - Email: email2, - IsVerified: false, - NextVerificationDate: dueDate, - } - - // add user to Cache - if userData, err := bson.Marshal(userStruct); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(email2), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(email2)) - - assert.Nil(t, err) - assert.NotNil(t, userA) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - isDue, err := database.CheckIfNextVerificationDateIsDue(ctx, appsession, email2) - - // Validate the result - assert.NoError(t, err) - assert.True(t, isDue) - }) - - mt.Run("FindOne error", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "find error", - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - isDue, err := database.CheckIfNextVerificationDateIsDue(ctx, appsession, email1) - - // Validate the result - assert.Error(t, err) - assert.False(t, isDue) - }) -} - -func TestCheckIfUserIsVerified(t *testing.T) { - // Setup mock MongoDB instance - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - gin.SetMode(configs.GetGinRunMode()) - - // Create a new HTTP request with the POST method. - req, _ := http.NewRequest("POST", "/", nil) - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a new context with the Request and ResponseWriter. - ctx, _ := gin.CreateTestContext(w) - ctx.Request = req - - // Optionally, set any values in the context. - ctx.Set("test", "test") - - email := "test@example.com" - - mt.Run("Nil database", func(mt *mtest.T) { - // Call the function under test - appsession := &models.AppSession{} - isVerified, err := database.CheckIfUserIsVerified(ctx, appsession, email) - - // Validate the result - assert.Error(t, err) - assert.False(t, isVerified) - }) - - mt.Run("User is verified", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "isVerified", Value: true}, - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - isVerified, err := database.CheckIfUserIsVerified(ctx, appsession, email) - - // Validate the result - assert.NoError(t, err) - assert.True(t, isVerified) - }) - - mt.Run("User is not verified", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "isVerified", Value: false}, - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - isVerified, err := database.CheckIfUserIsVerified(ctx, appsession, email) - - // Validate the result - assert.NoError(t, err) - assert.False(t, isVerified) - }) - - mt.Run("User is verified in cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "isVerified", Value: true}, - })) - - Cache := configs.CreateCache() - - userStruct := models.User{ - Email: email, - IsVerified: true, - } - - // add user to Cache - if userData, err := bson.Marshal(userStruct); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - isVerified, err := database.CheckIfUserIsVerified(ctx, appsession, email) - - // Validate the result - assert.NoError(t, err) - assert.True(t, isVerified) - }) - - mt.Run("User is not verified in cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "isVerified", Value: false}, - })) - - Cache := configs.CreateCache() - - userStruct := models.User{ - Email: email, - IsVerified: false, - } - - // add user to Cache - if userData, err := bson.Marshal(userStruct); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) - // Call the function under test appsession := &models.AppSession{ DB: mt.Client, @@ -1596,34 +1046,34 @@ func TestUpdateVerificationStatusTo(t *testing.T) { // Verify the update }) - mt.Run("Update verification status successfully in Cache to true", func(mt *mtest.T) { + mt.Run("Update verification status successfully in cache to true", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) - Cache := configs.CreateCache() + cache := configs.CreateCache() userStruct := models.User{ Email: email, IsVerified: false, } - // Add password to Cache + // Add password to cache if userData, err := bson.Marshal(userStruct); err != nil { t.Fatal(err) } else { - if err := Cache.Set(cache.UserKey(email), userData); err != nil { + if err := cache.Set(email, userData); err != nil { t.Fatal(err) } } - // Assert that the password is in the Cache - user, err := Cache.Get(cache.UserKey(email)) + // Assert that the password is in the cache + user, err := cache.Get(email) assert.Nil(t, err) assert.NotNil(t, user) appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } success, err := database.UpdateVerificationStatusTo(ctx, appsession, email, true) @@ -1632,8 +1082,8 @@ func TestUpdateVerificationStatusTo(t *testing.T) { assert.NoError(t, err) assert.True(t, success) - // Verify the update in Cache - user, err = Cache.Get(cache.UserKey(email)) + // Verify the update in cache + user, err = cache.Get(email) assert.Nil(t, err) assert.NotNil(t, user) @@ -1647,34 +1097,34 @@ func TestUpdateVerificationStatusTo(t *testing.T) { assert.True(t, userB.IsVerified) }) - mt.Run("Update verification status successfully in Cache to false", func(mt *mtest.T) { + mt.Run("Update verification status successfully in cache to false", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) - Cache := configs.CreateCache() + cache := configs.CreateCache() userStruct := models.User{ Email: email, IsVerified: true, } - // Add password to Cache + // Add password to cache if userData, err := bson.Marshal(userStruct); err != nil { t.Fatal(err) } else { - if err := Cache.Set(cache.UserKey(email), userData); err != nil { + if err := cache.Set(email, userData); err != nil { t.Fatal(err) } } - // Assert that the password is in the Cache - user, err := Cache.Get(cache.UserKey(email)) + // Assert that the password is in the cache + user, err := cache.Get(email) assert.Nil(t, err) assert.NotNil(t, user) appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } success, err := database.UpdateVerificationStatusTo(ctx, appsession, email, false) @@ -1683,8 +1133,8 @@ func TestUpdateVerificationStatusTo(t *testing.T) { assert.NoError(t, err) assert.True(t, success) - // Verify the update in Cache - user, err = Cache.Get(cache.UserKey(email)) + // Verify the update in cache + user, err = cache.Get(email) assert.Nil(t, err) assert.NotNil(t, user) @@ -1716,7 +1166,7 @@ func TestUpdateVerificationStatusTo(t *testing.T) { }) } -func TestConfirmCancellation(t *testing.T) { +func TestCheckIfUserIsAdmin(t *testing.T) { // Setup mock MongoDB instance mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) @@ -1735,257 +1185,132 @@ func TestConfirmCancellation(t *testing.T) { // Optionally, set any values in the context. ctx.Set("test", "test") - checkin := models.CheckIn{ - Creator: "test@example.com", - BookingID: "123456", - } + email := "test@example.com" mt.Run("Nil database", func(mt *mtest.T) { // Call the function under test appsession := &models.AppSession{} - success, err := database.ConfirmCancellation(ctx, appsession, checkin.BookingID, checkin.Creator) + isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession, email) // Validate the result assert.Error(t, err) - assert.False(t, success) + assert.False(t, isAdmin) }) - mt.Run("Confirm cancellation successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) + mt.Run("User is admin", func(mt *mtest.T) { + mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ + {Key: "email", Value: email}, + {Key: "role", Value: constants.Admin}, + })) // Call the function under test appsession := &models.AppSession{ DB: mt.Client, } - success, err := database.ConfirmCancellation(ctx, appsession, checkin.BookingID, checkin.Creator) + isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession, email) // Validate the result assert.NoError(t, err) - assert.True(t, success) - - // Verify the update + assert.True(t, isAdmin) }) - mt.Run("Confirm cancellation successfully in Cache", func(mt *mtest.T) { + mt.Run("User is admin in cache", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) - Cache := configs.CreateCache() + cache := configs.CreateCache() - bookingStruct := models.Booking{ - OccupiID: checkin.BookingID, - Creator: checkin.Creator, + userStruct := models.User{ + Email: email, + Role: constants.Admin, } - // Add checkin to Cache - if checkinData, err := bson.Marshal(bookingStruct); err != nil { + // Add user to cache + if userData, err := bson.Marshal(userStruct); err != nil { t.Fatal(err) } else { - if err := Cache.Set(cache.RoomBookingKey(bookingStruct.OccupiID), checkinData); err != nil { + if err := cache.Set(email, userData); err != nil { t.Fatal(err) } } - // Assert that the checkin is in the Cache - checkinv, err := Cache.Get(cache.RoomBookingKey(bookingStruct.OccupiID)) + // Assert that the user is in the cache + user, err := cache.Get(email) assert.Nil(t, err) - assert.NotNil(t, checkinv) + assert.NotNil(t, user) appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } // Call the function under test - success, err := database.ConfirmCancellation(ctx, appsession, checkin.BookingID, checkin.Creator) + isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession, email) // Validate the result assert.NoError(t, err) - assert.True(t, success) - - // Verify the update in Cache - checkinv2, err := Cache.Get(cache.RoomBookingKey(checkin.BookingID)) - - assert.NotNil(t, err) - assert.Nil(t, checkinv2) + assert.True(t, isAdmin) }) - mt.Run("UpdateOne error", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "update error", + mt.Run("User is not admin", func(mt *mtest.T) { + mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ + {Key: "email", Value: email}, + {Key: "role", Value: constants.Basic}, })) // Call the function under test appsession := &models.AppSession{ DB: mt.Client, } - success, err := database.ConfirmCancellation(ctx, appsession, checkin.BookingID, checkin.Creator) + isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession, email) // Validate the result - assert.Error(t, err) - assert.False(t, success) + assert.NoError(t, err) + assert.False(t, isAdmin) }) -} - -func TestGetUserDetails(t *testing.T) { - // Setup mock MongoDB instance - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - gin.SetMode(configs.GetGinRunMode()) + mt.Run("User is not admin in cache", func(mt *mtest.T) { + mt.AddMockResponses(mtest.CreateSuccessResponse()) - // Create a new HTTP request with the POST method. - req, _ := http.NewRequest("POST", "/", nil) + cache := configs.CreateCache() - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() + userStruct := models.User{ + Email: email, + Role: constants.Basic, + } - // Create a new context with the Request and ResponseWriter. - ctx, _ := gin.CreateTestContext(w) - ctx.Request = req - - // Optionally, set any values in the context. - ctx.Set("test", "test") - - userStruct := models.User{ - OccupiID: "123456", - Password: "hashedpassword123", - Email: "test@example.com", - Role: constants.Admin, - OnSite: true, - IsVerified: true, - NextVerificationDate: time.Now(), // this will be updated once the email is verified - TwoFAEnabled: false, - KnownLocations: []models.Location{ - { - City: "Cape Town", - Region: "Western Cape", - Country: "South Africa", - }, - }, - Details: models.Details{ - ImageID: "", - Name: "Michael", - DOB: time.Now(), - Gender: "Male", - Pronouns: "He/Him", - }, - Notifications: models.Notifications{ - Invites: true, - BookingReminder: true, - }, - Security: models.Security{ - MFA: false, - Biometrics: false, - ForceLogout: false, - }, - Status: "5", - Position: "Chief Executive Engineer", - DepartmentNo: "01", - ExpoPushToken: "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]", - } - - mt.Run("Nil database", func(mt *mtest.T) { - // Call the function under test - appsession := &models.AppSession{} - user, err := database.GetUserDetails(ctx, appsession, userStruct.Email) - - // empty user - usere := models.UserDetailsRequest{} - - // Validate the result - assert.Error(t, err) - assert.Equal(t, usere, user) - }) - - mt.Run("Get user details successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "occupiID", Value: userStruct.OccupiID}, - {Key: "password", Value: userStruct.Password}, - {Key: "email", Value: userStruct.Email}, - {Key: "role", Value: userStruct.Role}, - {Key: "onSite", Value: userStruct.OnSite}, - {Key: "isVerified", Value: userStruct.IsVerified}, - {Key: "nextVerificationDate", Value: userStruct.NextVerificationDate}, - {Key: "twoFAEnabled", Value: userStruct.TwoFAEnabled}, - {Key: "knownLocations", Value: userStruct.KnownLocations}, - {Key: "details", Value: userStruct.Details}, - {Key: "notifications", Value: userStruct.Notifications}, - {Key: "security", Value: userStruct.Security}, - {Key: "status", Value: userStruct.Status}, - {Key: "position", Value: userStruct.Position}, - {Key: "departmentNo", Value: userStruct.DepartmentNo}, - {Key: "expoPushToken", Value: userStruct.ExpoPushToken}, - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - user, err := database.GetUserDetails(ctx, appsession, userStruct.Email) - - expectedUser := models.UserDetailsRequest{ - Email: userStruct.Email, - Name: userStruct.Details.Name, - Gender: userStruct.Details.Gender, - Pronouns: userStruct.Details.Pronouns, - Number: userStruct.Details.ContactNo, - } - - // Validate the result - assert.NoError(t, err) - assert.NotNil(t, user) - assert.Equal(t, expectedUser.Email, user.Email) - assert.Equal(t, expectedUser.Name, user.Name) - assert.Equal(t, expectedUser.Gender, user.Gender) - assert.Equal(t, expectedUser.Pronouns, user.Pronouns) - assert.Equal(t, expectedUser.Number, user.Number) - }) - - mt.Run("Get user details successfully from Cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - // Add user to Cache + // Add user to cache if userData, err := bson.Marshal(userStruct); err != nil { t.Fatal(err) } else { - if err := Cache.Set(cache.UserKey(userStruct.Email), userData); err != nil { + if err := cache.Set(email, userData); err != nil { t.Fatal(err) } } - // Assert that the user is in the Cache - usera, err := Cache.Get(cache.UserKey(userStruct.Email)) + // Assert that the user is in the cache + user, err := cache.Get(email) assert.Nil(t, err) - assert.NotNil(t, usera) + assert.NotNil(t, user) appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } // Call the function under test - user, err := database.GetUserDetails(ctx, appsession, userStruct.Email) - - expectedUser := models.UserDetailsRequest{ - Email: userStruct.Email, - Name: userStruct.Details.Name, - Gender: userStruct.Details.Gender, - Pronouns: userStruct.Details.Pronouns, - Number: userStruct.Details.ContactNo, - } + isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession, email) // Validate the result assert.NoError(t, err) + assert.False(t, isAdmin) + + // Verify the user was not updated in the cache + user, err = cache.Get(email) + + assert.Nil(t, err) assert.NotNil(t, user) - assert.Equal(t, expectedUser.Email, user.Email) - assert.Equal(t, expectedUser.Name, user.Name) - assert.Equal(t, expectedUser.Gender, user.Gender) - assert.Equal(t, expectedUser.Pronouns, user.Pronouns) - assert.Equal(t, expectedUser.Number, user.Number) }) mt.Run("FindOne error", func(mt *mtest.T) { @@ -1998,2745 +1323,841 @@ func TestGetUserDetails(t *testing.T) { appsession := &models.AppSession{ DB: mt.Client, } - user, err := database.GetUserDetails(ctx, appsession, userStruct.Email) - - // empty user - usere := models.UserDetailsRequest{} + isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession, email) // Validate the result assert.Error(t, err) - assert.Equal(t, usere, user) + assert.False(t, isAdmin) }) } -func TestUpdateUserDetails(t *testing.T) { +// Test AddResetToken +func TestAddResetToken(t *testing.T) { mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - // set gin run mode - gin.SetMode(configs.GetGinRunMode()) - ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - - mt.Run("Nil database", func(mt *mtest.T) { - // Call the function under test - appsession := &models.AppSession{} - success, err := database.UpdateUserDetails(ctx, appsession, models.UserDetailsRequest{}) - - // Validate the result - assert.Error(t, err) - assert.False(t, success) - }) - - mt.Run("Update user name successfully", func(mt *mtest.T) { + mt.Run("success", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) - userDetails := models.UserDetailsRequest{ - SessionEmail: "test@example.com", - Name: "Michael", - } - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } + email := "test@example.com" + resetToken := "token123" + expirationTime := time.Now().Add(1 * time.Hour) - success, err := database.UpdateUserDetails(ctx, appsession, userDetails) + success, err := database.AddResetToken(context.Background(), mt.Client, email, resetToken, expirationTime) - // Validate the result assert.NoError(t, err) assert.True(t, success) }) - mt.Run("Update user name in Cache successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() + mt.Run("error", func(mt *mtest.T) { + mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ + Code: 11000, + Message: "duplicate key error", + })) - userDetails := models.User{ - Email: "test@example.com", - Details: models.Details{ - Name: "null", - }, - } + email := "test@example.com" + resetToken := "token123" + expirationTime := time.Now().Add(1 * time.Hour) - // Add user to Cache - if userData, err := bson.Marshal(userDetails); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(userDetails.Email), userData); err != nil { - t.Fatal(err) - } - } + success, err := database.AddResetToken(context.Background(), mt.Client, email, resetToken, expirationTime) - // Assert that the user is in the Cache - user, err := Cache.Get(cache.UserKey(userDetails.Email)) + assert.Error(t, err) + assert.False(t, success) + }) +} - assert.Nil(t, err) - assert.NotNil(t, user) +// Test GetEmailByResetToken +func TestGetEmailByResetToken(t *testing.T) { + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } + mt.Run("success", func(mt *mtest.T) { + expectedEmail := "test@example.com" + resetToken := "token123" - updateUser := models.UserDetailsRequest{ - SessionEmail: userDetails.Email, - Name: "Michael", - } + mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".ResetTokens", mtest.FirstBatch, bson.D{ + {Key: "email", Value: expectedEmail}, + {Key: "token", Value: resetToken}, + })) - // Call the function under test - success, err := database.UpdateUserDetails(ctx, appsession, updateUser) + email, err := database.GetEmailByResetToken(context.Background(), mt.Client, resetToken) - // Validate the result assert.NoError(t, err) - assert.True(t, success) + assert.Equal(t, expectedEmail, email) + }) - // Verify the update in Cache - user, err = Cache.Get(cache.UserKey(userDetails.Email)) + mt.Run("not found", func(mt *mtest.T) { + resetToken := "nonexistenttoken" - assert.Nil(t, err) - assert.NotNil(t, user) + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".ResetTokens", mtest.FirstBatch)) - // unmarshal the user data - var userB models.User - if err := bson.Unmarshal(user, &userB); err != nil { - t.Fatal(err) - } + email, err := database.GetEmailByResetToken(context.Background(), mt.Client, resetToken) - assert.Equal(t, updateUser.Name, userB.Details.Name) + assert.Error(t, err) + assert.Equal(t, "", email) }) +} - mt.Run("Update user dob successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) +// Test CheckResetToken - userDetails := models.UserDetailsRequest{ - SessionEmail: "test@example.com", - Dob: time.Now().String(), - } +func TestCheckResetToken(t *testing.T) { + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } + gin.SetMode(gin.TestMode) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - success, err := database.UpdateUserDetails(ctx, appsession, userDetails) + mt.Run("valid token", func(mt *mtest.T) { + email := "test@example.com" + token := "validtoken" + expireWhen := time.Now().Add(1 * time.Hour) - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - }) + mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".ResetTokens", mtest.FirstBatch, bson.D{ + {Key: "email", Value: email}, + {Key: "token", Value: token}, + {Key: "expireWhen", Value: expireWhen}, + })) - mt.Run("Update user dob in Cache successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) + valid, err := database.CheckResetToken(ctx, mt.Client, email, token) - Cache := configs.CreateCache() + assert.NoError(t, err) + assert.True(t, valid) + }) - userDetails := models.User{ - Email: "test@example.com", - Details: models.Details{ - DOB: time.Now(), - }, - } + mt.Run("expired token", func(mt *mtest.T) { + email := "test@example.com" + token := "expiredtoken" + expireWhen := time.Now().Add(-1 * time.Hour) - // Add user to Cache - if userData, err := bson.Marshal(userDetails); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(userDetails.Email), userData); err != nil { - t.Fatal(err) - } - } + mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".ResetTokens", mtest.FirstBatch, bson.D{ + {Key: "email", Value: email}, + {Key: "token", Value: token}, + {Key: "expireWhen", Value: expireWhen}, + })) - // Assert that the user is in the Cache - user, err := Cache.Get(cache.UserKey(userDetails.Email)) + valid, err := database.CheckResetToken(ctx, mt.Client, email, token) - assert.Nil(t, err) - assert.NotNil(t, user) + assert.NoError(t, err) + assert.False(t, valid) + }) - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } + mt.Run("token not found", func(mt *mtest.T) { + email := "test@example.com" + token := "nonexistenttoken" - updateUser := models.UserDetailsRequest{ - SessionEmail: userDetails.Email, - Dob: time.Now().Add(1 * time.Hour).String(), - } + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".ResetTokens", mtest.FirstBatch)) - // Call the function under test - success, err := database.UpdateUserDetails(ctx, appsession, updateUser) + valid, err := database.CheckResetToken(ctx, mt.Client, email, token) - // Validate the result + assert.Error(t, err) + assert.False(t, valid) + }) +} - assert.NoError(t, err) - assert.True(t, success) +// Test UpdateUserPassword +func TestUpdateUserPassword(t *testing.T) { + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - // Verify the update in Cache - user, err = Cache.Get(cache.UserKey(userDetails.Email)) + gin.SetMode(gin.TestMode) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - assert.Nil(t, err) - assert.NotNil(t, user) - }) + mt.Run("success", func(mt *mtest.T) { + email := "test@example.com" + newPassword := "newpassword123" - mt.Run("Update gender successfully", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) - userDetails := models.UserDetailsRequest{ - SessionEmail: "test@example.com", - Gender: "Male", - } - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - - success, err := database.UpdateUserDetails(ctx, appsession, userDetails) + success, err := database.UpdateUserPassword(ctx, mt.Client, email, newPassword) - // Validate the result assert.NoError(t, err) assert.True(t, success) }) - mt.Run("Update gender in Cache successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) + mt.Run("error", func(mt *mtest.T) { + email := "test@example.com" + newPassword := "newpassword123" - Cache := configs.CreateCache() + mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ + Code: 11000, + Message: "update error", + })) - userDetails := models.User{ - Email: "test@example.com", - Details: models.Details{ - Gender: "null", - }, - } + success, err := database.UpdateUserPassword(ctx, mt.Client, email, newPassword) - // Add user to Cache - if userData, err := bson.Marshal(userDetails); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(userDetails.Email), userData); err != nil { - t.Fatal(err) - } - } + assert.Error(t, err) + assert.False(t, success) + }) +} - // Assert that the user is in the Cache - user, err := Cache.Get(cache.UserKey(userDetails.Email)) +// Test ClearResetToken +func TestClearResetToken(t *testing.T) { + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - assert.Nil(t, err) - assert.NotNil(t, user) + gin.SetMode(gin.TestMode) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } + mt.Run("success", func(mt *mtest.T) { + email := "test@example.com" + token := "token123" - updateUser := models.UserDetailsRequest{ - SessionEmail: userDetails.Email, - Gender: "Male", - } + mt.AddMockResponses(mtest.CreateSuccessResponse()) - // Call the function under test - success, err := database.UpdateUserDetails(ctx, appsession, updateUser) + success, err := database.ClearResetToken(ctx, mt.Client, email, token) - // Validate the result assert.NoError(t, err) assert.True(t, success) - - // Verify the update in Cache - user, err = Cache.Get(cache.UserKey(userDetails.Email)) - - assert.Nil(t, err) - assert.NotNil(t, user) - - // unmarshal the user data - var userB models.User - if err := bson.Unmarshal(user, &userB); err != nil { - t.Fatal(err) - } - - assert.Equal(t, updateUser.Gender, userB.Details.Gender) - }) - - mt.Run("Update email successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - userDetails := models.UserDetailsRequest{ - SessionEmail: "test@example.com", - Email: "test@example.com", - } - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - - success, err := database.UpdateUserDetails(ctx, appsession, userDetails) - - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - }) - - mt.Run("Update email in Cache successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - userDetails := models.User{ - Email: "test@example.com", - IsVerified: true, - } - - // Add user to Cache - if userData, err := bson.Marshal(userDetails); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(userDetails.Email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - user, err := Cache.Get(cache.UserKey(userDetails.Email)) - - assert.Nil(t, err) - assert.NotNil(t, user) - - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - - updateUser := models.UserDetailsRequest{ - SessionEmail: userDetails.Email, - Email: "test1@example.com", - } - - // Call the function under test - success, err := database.UpdateUserDetails(ctx, appsession, updateUser) - - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - - // Verify the update in Cache - user, err = Cache.Get(cache.UserKey(updateUser.Email)) - - assert.Nil(t, err) - assert.NotNil(t, user) - - // unmarshal the user data - var userB models.User - if err := bson.Unmarshal(user, &userB); err != nil { - t.Fatal(err) - } - - assert.Equal(t, updateUser.Email, userB.Email) - assert.False(t, userB.IsVerified) - }) - - mt.Run("Update employee id successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - userDetails := models.UserDetailsRequest{ - SessionEmail: "test@example.com", - Employeeid: "123456", - } - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - - success, err := database.UpdateUserDetails(ctx, appsession, userDetails) - - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - }) - - mt.Run("Update employee id in Cache successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - userDetails := models.User{ - Email: "test@example.com", - OccupiID: "null", - } - - // Add user to Cache - if userData, err := bson.Marshal(userDetails); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(userDetails.Email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - user, err := Cache.Get(cache.UserKey(userDetails.Email)) - - assert.Nil(t, err) - assert.NotNil(t, user) - - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - - updateUser := models.UserDetailsRequest{ - SessionEmail: userDetails.Email, - Employeeid: "123456", - } - - // Call the function under test - success, err := database.UpdateUserDetails(ctx, appsession, updateUser) - - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - - // Verify the update in Cache - user, err = Cache.Get(cache.UserKey(userDetails.Email)) - - assert.Nil(t, err) - assert.NotNil(t, user) - - // unmarshal the user data - var userB models.User - if err := bson.Unmarshal(user, &userB); err != nil { - t.Fatal(err) - } - - assert.Equal(t, updateUser.Employeeid, userB.OccupiID) - }) - - mt.Run("Update number successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - userDetails := models.UserDetailsRequest{ - SessionEmail: "test@example.com", - Number: "011 123 4567", - } - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - - success, err := database.UpdateUserDetails(ctx, appsession, userDetails) - - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - }) - - mt.Run("Update email in Cache successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - userDetails := models.User{ - Email: "test@example.com", - Details: models.Details{ - ContactNo: "null", - }, - } - - // Add user to Cache - if userData, err := bson.Marshal(userDetails); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(userDetails.Email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - user, err := Cache.Get(cache.UserKey(userDetails.Email)) - - assert.Nil(t, err) - assert.NotNil(t, user) - - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - - updateUser := models.UserDetailsRequest{ - SessionEmail: userDetails.Email, - Number: "011 123 4567", - } - - // Call the function under test - success, err := database.UpdateUserDetails(ctx, appsession, updateUser) - - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - - // Verify the update in Cache - user, err = Cache.Get(cache.UserKey(userDetails.Email)) - - assert.Nil(t, err) - assert.NotNil(t, user) - - // unmarshal the user data - var userB models.User - if err := bson.Unmarshal(user, &userB); err != nil { - t.Fatal(err) - } - - assert.Equal(t, updateUser.Number, userB.Details.ContactNo) - }) - - mt.Run("Update pronouns successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - userDetails := models.UserDetailsRequest{ - SessionEmail: "test@example.com", - Pronouns: "He/Him", - } - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - - success, err := database.UpdateUserDetails(ctx, appsession, userDetails) - - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - }) - - mt.Run("Update pronouns in Cache successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - userDetails := models.User{ - Email: "test@example.com", - Details: models.Details{ - Pronouns: "null", - }, - } - - // Add user to Cache - if userData, err := bson.Marshal(userDetails); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(userDetails.Email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - user, err := Cache.Get(cache.UserKey(userDetails.Email)) - - assert.Nil(t, err) - assert.NotNil(t, user) - - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - - updateUser := models.UserDetailsRequest{ - SessionEmail: userDetails.Email, - Pronouns: "He/Him", - } - - // Call the function under test - success, err := database.UpdateUserDetails(ctx, appsession, updateUser) - - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - - // Verify the update in Cache - user, err = Cache.Get(cache.UserKey(userDetails.Email)) - - assert.Nil(t, err) - assert.NotNil(t, user) - - // unmarshal the user data - var userB models.User - if err := bson.Unmarshal(user, &userB); err != nil { - t.Fatal(err) - } - - assert.Equal(t, updateUser.Pronouns, userB.Details.Pronouns) - }) - - mt.Run("Update Error", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "update error", - })) - - userDetails := models.UserDetailsRequest{ - SessionEmail: "test@example.com", - Name: "Michael", - } - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - - success, err := database.UpdateUserDetails(ctx, appsession, userDetails) - - // Validate the result - assert.Error(t, err) - assert.False(t, success) - }) -} - -func TestCheckIfUserIsAdmin(t *testing.T) { - // Setup mock MongoDB instance - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - gin.SetMode(configs.GetGinRunMode()) - - // Create a new HTTP request with the POST method. - req, _ := http.NewRequest("POST", "/", nil) - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a new context with the Request and ResponseWriter. - ctx, _ := gin.CreateTestContext(w) - ctx.Request = req - - // Optionally, set any values in the context. - ctx.Set("test", "test") - - email := "test@example.com" - - mt.Run("Nil database", func(mt *mtest.T) { - // Call the function under test - appsession := &models.AppSession{} - isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession, email) - - // Validate the result - assert.Error(t, err) - assert.False(t, isAdmin) - }) - - mt.Run("User is admin", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "role", Value: constants.Admin}, - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession, email) - - // Validate the result - assert.NoError(t, err) - assert.True(t, isAdmin) - }) - - mt.Run("User is admin in Cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - userStruct := models.User{ - Email: email, - Role: constants.Admin, - } - - // Add user to Cache - if userData, err := bson.Marshal(userStruct); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - user, err := Cache.Get(cache.UserKey(email)) - - assert.Nil(t, err) - assert.NotNil(t, user) - - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - - // Call the function under test - isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession, email) - - // Validate the result - assert.NoError(t, err) - assert.True(t, isAdmin) - }) - - mt.Run("User is not admin", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "role", Value: constants.Basic}, - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession, email) - - // Validate the result - assert.NoError(t, err) - assert.False(t, isAdmin) - }) - - mt.Run("User is not admin in Cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - userStruct := models.User{ - Email: email, - Role: constants.Basic, - } - - // Add user to Cache - if userData, err := bson.Marshal(userStruct); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - user, err := Cache.Get(cache.UserKey(email)) - - assert.Nil(t, err) - assert.NotNil(t, user) - - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - - // Call the function under test - isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession, email) - - // Validate the result - assert.NoError(t, err) - assert.False(t, isAdmin) - - // Verify the user was not updated in the Cache - user, err = Cache.Get(cache.UserKey(email)) - - assert.Nil(t, err) - assert.NotNil(t, user) - }) - - mt.Run("FindOne error", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "find error", - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession, email) - - // Validate the result - assert.Error(t, err) - assert.False(t, isAdmin) - }) -} - -// Test AddResetToken -func TestAddResetToken(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - mt.Run("success", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - email := "test@example.com" - resetToken := "token123" - expirationTime := time.Now().Add(1 * time.Hour) - - success, err := database.AddResetToken(context.Background(), mt.Client, email, resetToken, expirationTime) - - assert.NoError(t, err) - assert.True(t, success) - }) - - mt.Run("error", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "duplicate key error", - })) - - email := "test@example.com" - resetToken := "token123" - expirationTime := time.Now().Add(1 * time.Hour) - - success, err := database.AddResetToken(context.Background(), mt.Client, email, resetToken, expirationTime) - - assert.Error(t, err) - assert.False(t, success) - }) -} - -// Test GetEmailByResetToken -func TestGetEmailByResetToken(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - mt.Run("success", func(mt *mtest.T) { - expectedEmail := "test@example.com" - resetToken := "token123" - - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".ResetTokens", mtest.FirstBatch, bson.D{ - {Key: "email", Value: expectedEmail}, - {Key: "token", Value: resetToken}, - })) - - email, err := database.GetEmailByResetToken(context.Background(), mt.Client, resetToken) - - assert.NoError(t, err) - assert.Equal(t, expectedEmail, email) - }) - - mt.Run("not found", func(mt *mtest.T) { - resetToken := "nonexistenttoken" - - mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".ResetTokens", mtest.FirstBatch)) - - email, err := database.GetEmailByResetToken(context.Background(), mt.Client, resetToken) - - assert.Error(t, err) - assert.Equal(t, "", email) - }) -} - -// Test CheckResetToken -func TestCheckResetToken(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - // set gin run mode - gin.SetMode(configs.GetGinRunMode()) - ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - - mt.Run("valid token", func(mt *mtest.T) { - email := "test@example.com" - token := "validtoken" - expireWhen := time.Now().Add(1 * time.Hour) - - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".ResetTokens", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "token", Value: token}, - {Key: "expireWhen", Value: expireWhen}, - })) - - valid, err := database.CheckResetToken(ctx, mt.Client, email, token) - - assert.NoError(t, err) - assert.True(t, valid) - }) - - mt.Run("expired token", func(mt *mtest.T) { - email := "test@example.com" - token := "expiredtoken" - expireWhen := time.Now().Add(-1 * time.Hour) - - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".ResetTokens", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "token", Value: token}, - {Key: "expireWhen", Value: expireWhen}, - })) - - valid, err := database.CheckResetToken(ctx, mt.Client, email, token) - - assert.NoError(t, err) - assert.False(t, valid) - }) - - mt.Run("token not found", func(mt *mtest.T) { - email := "test@example.com" - token := "nonexistenttoken" - - mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".ResetTokens", mtest.FirstBatch)) - - valid, err := database.CheckResetToken(ctx, mt.Client, email, token) - - assert.Error(t, err) - assert.False(t, valid) - }) -} - -// Test UpdateUserPassword -func TestUpdateUserPassword(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - // set gin run mode - gin.SetMode(configs.GetGinRunMode()) - ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - - mt.Run("nil database", func(mt *mtest.T) { - email := "test@example.com" - newPassword := "newpassword123" - - appsession := &models.AppSession{} - success, err := database.UpdateUserPassword(ctx, appsession, email, newPassword) - - assert.Error(t, err) - assert.False(t, success) - }) - - mt.Run("success", func(mt *mtest.T) { - email := "test@example.com" - newPassword := "newpassword123" - - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - appsession := &models.AppSession{ - DB: mt.Client, - } - success, err := database.UpdateUserPassword(ctx, appsession, email, newPassword) - - assert.NoError(t, err) - assert.True(t, success) - }) - - mt.Run("Update user password successfully in Cache", func(mt *mtest.T) { - email := "test@example.com" - newPassword := "newpassword123" - - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - userStruct := models.User{ - Email: email, - Password: "oldpassword", - } - - // add user to Cache - if userData, err := bson.Marshal(userStruct); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - - success, err := database.UpdateUserPassword(ctx, appsession, email, newPassword) - - // Validate the result - assert.NoError(t, err) - assert.True(t, success) - - // Verify the update in Cache - userB, err := Cache.Get(cache.UserKey(email)) - - assert.Nil(t, err) - assert.NotNil(t, userB) - - // unmarshal the user data - var user models.User - if err := bson.Unmarshal(userB, &user); err != nil { - t.Fatal(err) - } - - assert.Equal(t, newPassword, user.Password) - }) - - mt.Run("error", func(mt *mtest.T) { - email := "test@example.com" - newPassword := "newpassword123" - - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "update error", - })) - - appsession := &models.AppSession{ - DB: mt.Client, - } - success, err := database.UpdateUserPassword(ctx, appsession, email, newPassword) - - assert.Error(t, err) - assert.False(t, success) - }) -} - -// Test ClearResetToken -func TestClearResetToken(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - // set gin run mode - gin.SetMode(configs.GetGinRunMode()) - ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - - mt.Run("success", func(mt *mtest.T) { - email := "test@example.com" - token := "token123" - - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - success, err := database.ClearResetToken(ctx, mt.Client, email, token) - - assert.NoError(t, err) - assert.True(t, success) - }) - - mt.Run("error", func(mt *mtest.T) { - email := "test@example.com" - token := "token123" - - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "delete error", - })) - - success, err := database.ClearResetToken(ctx, mt.Client, email, token) - - assert.Error(t, err) - assert.False(t, success) - }) -} - -func TestFilterUsersWithProjection(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - tests := []struct { - name string - appSession *models.AppSession - filter primitive.M - projection bson.M - limit int64 - skip int64 - mockResponses []bson.D - expectedResult []bson.M - expectedCount int64 - expectedError error - }{ - { - name: "Database is nil", - appSession: &models.AppSession{ - DB: nil, - }, - filter: primitive.M{"username": "testuser"}, - projection: bson.M{"username": 1, "email": 1}, - limit: 10, - skip: 0, - expectedResult: nil, - expectedCount: 0, - expectedError: errors.New("database is nil"), - }, - { - name: "Error in cursor all", - appSession: &models.AppSession{ - DB: mt.Client, - }, - filter: primitive.M{"username": "testuser"}, - projection: bson.M{"username": 1, "email": 1}, - limit: 10, - skip: 0, - mockResponses: nil, - expectedResult: nil, - expectedCount: 0, - expectedError: errors.New("database is nil"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mt.Run("mock "+tt.name, func(mt *mtest.T) { - // Add mock responses - mt.AddMockResponses(tt.mockResponses...) - - // Setup Gin context - gin.SetMode(configs.GetGinRunMode()) - ctx, _ := gin.CreateTestContext(nil) - - filter := models.FilterStruct{ - Filter: tt.filter, - Projection: tt.projection, - Limit: tt.limit, - Skip: tt.skip, - } - - // Execute the function - results, count, err := database.FilterCollectionWithProjection(ctx, tt.appSession, "Users", filter) - - // Validate results - if !reflect.DeepEqual(results, tt.expectedResult) { - t.Errorf("FilterUsersWithProjection() got = %v, want %v", results, tt.expectedResult) - } - if count != tt.expectedCount { - t.Errorf("FilterUsersWithProjection() count = %v, want %v", count, tt.expectedCount) - } - if err != nil && tt.expectedError != nil && err.Error() != tt.expectedError.Error() { - t.Errorf("FilterUsersWithProjection() error = %v, want %v", err, tt.expectedError) - } - if err != nil && tt.expectedError == nil { - t.Errorf("FilterUsersWithProjection() unexpected error = %v", err) - } - }) - }) - } -} - -func TestFilterUsersWithProjectionSuccess(t *testing.T) { - email := "TestFilterUsersWithProjectionSuccess@example.com" - // Create database connection and Cache - db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a response writer and context - ctx, _ := gin.CreateTestContext(w) - - // Create a new AppSession with the Cache - appSession := &models.AppSession{ - DB: db, - } - - // Mock the DB response - collection := db.Database(configs.GetMongoDBName()).Collection("Users") - - filter := primitive.M{"email": email} - projection := bson.M{"email": 1} - limit := 10 - skip := 0 - - // Create test users - users := []models.UserDetails{ - { - Email: email, - }, - { - Email: email + ".za", - }, - { - Email: email + ".uk", - }, - { - Email: email + ".us", - }, - } - - // Insert test users into the database - for _, user := range users { - _, err := collection.InsertOne(ctx, user) - - if err != nil { - t.Fatalf("Failed to insert test user into database: %v", err) - } - } - - filter_arg := models.FilterStruct{ - Filter: filter, - Projection: projection, - Limit: int64(limit), - Skip: int64(skip), - } - - // Execute the function - results, count, err := database.FilterCollectionWithProjection(ctx, appSession, "Users", filter_arg) - - // Validate results - if err != nil { - t.Fatalf("FilterUsersWithProjection() error = %v", err) - } - - if count != 1 { - t.Fatalf("FilterUsersWithProjection() count = %v, want %v", count, 1) - } - - if len(results) != 1 { - t.Fatalf("FilterUsersWithProjection() results count = %v, want %v", len(results), 1) - } - - if results[0]["email"] != email { - t.Fatalf("FilterUsersWithProjection() email = %v, want %v", results[0]["email"], email) - } -} - -func TestFilterUsersWithProjectionAndSortAscSuccess(t *testing.T) { - // Create database connection and Cache - db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a response writer and context - ctx, _ := gin.CreateTestContext(w) - - // Create a new AppSession with the Cache - appSession := &models.AppSession{ - DB: db, - } - - // Mock the DB response - collection := db.Database(configs.GetMongoDBName()).Collection("Users") - - filter := primitive.M{ - "email": primitive.Regex{ - Pattern: "^" + "TestFilterUsersWithProjectionAndSortAscSuccess", // "^" ensures the pattern matches the beginning of the string - Options: "i", // "i" makes the regex case-insensitive (optional) - }, - } - projection := bson.M{"email": 1} - limit := 10 - skip := 0 - sort := bson.M{"email": 1} - - // Create test users - users := []models.UserDetails{ - { - Email: "TestFilterUsersWithProjectionAndSortAscSuccess3@example.com", - }, - { - Email: "TestFilterUsersWithProjectionAndSortAscSuccess2@example.com", - }, - { - Email: "TestFilterUsersWithProjectionAndSortAscSuccess1@example.com", - }, - { - Email: "TestFilterUsersWithProjectionAndSortAscSuccess4@example.com", - }, - } - - // Insert test users into the database - for _, user := range users { - _, err := collection.InsertOne(ctx, user) - - if err != nil { - t.Fatalf("Failed to insert test user into database: %v", err) - } - } - - filter_arg := models.FilterStruct{ - Filter: filter, - Projection: projection, - Limit: int64(limit), - Skip: int64(skip), - Sort: sort, - } - - // Execute the function - results, count, err := database.FilterCollectionWithProjection(ctx, appSession, "Users", filter_arg) - - // Validate results - if err != nil { - t.Fatalf("FilterUsersWithProjection() error = %v", err) - } - - if count != 4 { - t.Fatalf("FilterUsersWithProjection() count = %v, want %v", count, 1) - } - - if len(results) != 4 { - t.Fatalf("FilterUsersWithProjection() results count = %v, want %v", len(results), 1) - } - - if results[0]["email"] != "TestFilterUsersWithProjectionAndSortAscSuccess1@example.com" { - t.Fatalf("FilterUsersWithProjection() email = %v, want %v", results[0]["email"], "TestFilterUsersWithProjectionAndSortAscSuccess1@example.com") - } -} - -func TestFilterUsersWithProjectionAndSortDescSuccess(t *testing.T) { - // Create database connection and Cache - db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a response writer and context - ctx, _ := gin.CreateTestContext(w) - - // Create a new AppSession with the Cache - appSession := &models.AppSession{ - DB: db, - } - - // Mock the DB response - collection := db.Database(configs.GetMongoDBName()).Collection("Users") - - filter := primitive.M{ - "email": primitive.Regex{ - Pattern: "^" + "TestFilterUsersWithProjectionAndSortDescSuccess", // "^" ensures the pattern matches the beginning of the string - Options: "i", // "i" makes the regex case-insensitive (optional) - }, - } - projection := bson.M{"email": 1} - limit := 10 - skip := 0 - sort := bson.M{"email": -1} - - // Create test users - users := []models.UserDetails{ - { - Email: "TestFilterUsersWithProjectionAndSortDescSuccess3@example.com", - }, - { - Email: "TestFilterUsersWithProjectionAndSortDescSuccess2@example.com", - }, - { - Email: "TestFilterUsersWithProjectionAndSortDescSuccess1@example.com", - }, - { - Email: "TestFilterUsersWithProjectionAndSortDescSuccess4@example.com", - }, - } - - // Insert test users into the database - for _, user := range users { - _, err := collection.InsertOne(ctx, user) - - if err != nil { - t.Fatalf("Failed to insert test user into database: %v", err) - } - } - - filter_arg := models.FilterStruct{ - Filter: filter, - Projection: projection, - Limit: int64(limit), - Skip: int64(skip), - Sort: sort, - } - - // Execute the function - results, count, err := database.FilterCollectionWithProjection(ctx, appSession, "Users", filter_arg) - - // Validate results - if err != nil { - t.Fatalf("FilterUsersWithProjection() error = %v", err) - } - - if count != 4 { - t.Fatalf("FilterUsersWithProjection() count = %v, want %v", count, 1) - } - - if len(results) != 4 { - t.Fatalf("FilterUsersWithProjection() results count = %v, want %v", len(results), 1) - } - - if results[0]["email"] != "TestFilterUsersWithProjectionAndSortDescSuccess4@example.com" { - t.Fatalf("FilterUsersWithProjection() email = %v, want %v", results[0]["email"], "TestFilterUsersWithProjectionAndSortDescSuccess4@example.com") - } -} - -func TestCheckIfUserIsLoggingInFromKnownLocation(t *testing.T) { - // Setup mock MongoDB instance - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - gin.SetMode(configs.GetGinRunMode()) - - // Create a new HTTP request with the POST method. - req, _ := http.NewRequest("POST", "/", nil) - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a new context with the Request and ResponseWriter. - ctx, _ := gin.CreateTestContext(w) - ctx.Request = req - - // Optionally, set any values in the context. - ctx.Set("test", "test") - - email := "test@example.com" - - mt.Run("Nil database", func(mt *mtest.T) { - // Call the function under test - appsession := &models.AppSession{} - yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) - - // Validate the result - assert.Error(t, err) - assert.False(t, yes) - assert.Nil(t, info) - }) - - mt.Run("Location does not exist", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) - - // Validate the result - assert.NoError(t, err) - assert.False(t, yes) - assert.NotNil(t, info) - assert.Equal(t, "Cape Town", info.City) - assert.Equal(t, "Western Cape", info.Region) - assert.Equal(t, "South Africa", info.Country) - }) - - mt.Run("Location exists and is valid", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "knownLocations", Value: bson.A{ - bson.D{ - {Key: "city", Value: "Cape Town"}, - {Key: "region", Value: "Western Cape"}, - {Key: "country", Value: "South Africa"}, - }, - }}, - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) - - // Validate the result - assert.NoError(t, err) - assert.True(t, yes) - assert.Nil(t, info) - }) - - mt.Run("Location exists but ip address unkwown", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "knownLocations", Value: bson.A{ - bson.D{ - {Key: "city", Value: "Durban"}, - {Key: "region", Value: "KwaZulu-Natal"}, - {Key: "country", Value: "South Africa"}, - }, - }}, - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) - - // Validate the result - assert.NoError(t, err) - assert.False(t, yes) - assert.NotNil(t, info) - assert.Equal(t, "Cape Town", info.City) - assert.Equal(t, "Western Cape", info.Region) - assert.Equal(t, "South Africa", info.Country) - }) - - mt.Run("Location exists and is valid in Cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - userStruct := models.User{ - Email: email, - KnownLocations: []models.Location{ - { - City: "Cape Town", - Region: "Western Cape", - Country: "South Africa", - }, - }, - } - - // add userstruct to Cache - if userData, err := bson.Marshal(userStruct); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) - - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - - // Call the function under test - yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) - - // Validate the result - assert.NoError(t, err) - assert.True(t, yes) - assert.Nil(t, info) - }) - - mt.Run("Location exists but does not match what is in Cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - userStruct := models.User{ - Email: email, - KnownLocations: []models.Location{ - { - City: "Durban", - Region: "KwaZulu-Natal", - Country: "South Africa", - }, - }, - } - - // add userstruct to Cache - if userData, err := bson.Marshal(userStruct); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) - - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - - // Call the function under test - yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) - - // Validate the result - assert.NoError(t, err) - assert.False(t, yes) - assert.NotNil(t, info) - assert.Equal(t, "Cape Town", info.City) - assert.Equal(t, "Western Cape", info.Region) - assert.Equal(t, "South Africa", info.Country) - }) - - mt.Run("Handle find error", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "find error", - })) - - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) - - // Validate the result - assert.Error(t, err) - assert.False(t, yes) - assert.Nil(t, info) - }) -} - -func TestGetUsersPushTokens(t *testing.T) { - users := []models.User{ - { - Email: "TestGetUsersPushTokens1@example.com", - ExpoPushToken: "b1b2b3b4b5b6b7b8b9b0", - Notifications: models.Notifications{ - Invites: true, - }, - }, - { - Email: "TestGetUsersPushTokens2@example.com", - ExpoPushToken: "a1a2a3a4a5a6a7a8a9a0", - Notifications: models.Notifications{ - Invites: true, - }, - }, - } - // Create database connection and Cache - db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() - - // Create a response writer and context - ctx, _ := gin.CreateTestContext(w) - - // Create a new AppSession with the Cache - appSession := &models.AppSession{ - DB: db, - } - - // Mock the DB response - collection := db.Database(configs.GetMongoDBName()).Collection("Users") - - // Insert test users into the database - for _, user := range users { - _, err := collection.InsertOne(ctx, user) - - if err != nil { - t.Fatalf("Failed to insert test user into database: %v", err) - } - } - - // Test case: Database is nil - t.Run("Database is nil", func(t *testing.T) { - emails := []string{"test@example.com"} - - appsession := &models.AppSession{} - results, err := database.GetUsersPushTokens(ctx, appsession, emails) - assert.Nil(t, results) - assert.EqualError(t, err, "database is nil") - }) - - // Test case: Empty emails - t.Run("Empty emails", func(t *testing.T) { - emails := []string{} - results, err := database.GetUsersPushTokens(ctx, appSession, emails) - - assert.Nil(t, results) - assert.NotNil(t, err) - assert.EqualError(t, err, "no emails provided") - }) - - // Test case: No matching users - t.Run("No matching users", func(t *testing.T) { - emails := []string{"TestGetUsersPushTokens3@example.com"} - results, err := database.GetUsersPushTokens(ctx, appSession, emails) - - assert.Nil(t, results) - assert.Nil(t, err) - }) - - // Test case: Successful query with one user - t.Run("Successful query with one user a", func(t *testing.T) { - emails := []string{"TestGetUsersPushTokens1@example.com"} - results, err := database.GetUsersPushTokens(ctx, appSession, emails) - - assert.NoError(t, err) - assert.Len(t, results, 1) - assert.Equal(t, users[0].ExpoPushToken, results[0]["expoPushToken"]) - }) - - t.Run("Successful query with one user b", func(t *testing.T) { - emails := []string{"TestGetUsersPushTokens2@example.com"} - results, err := database.GetUsersPushTokens(ctx, appSession, emails) - - assert.NoError(t, err) - assert.Len(t, results, 1) - assert.Equal(t, users[1].ExpoPushToken, results[0]["expoPushToken"]) - }) - - // Test case: Successful query with two users - t.Run("Successful query with two users", func(t *testing.T) { - emails := []string{"TestGetUsersPushTokens1@example.com", "TestGetUsersPushTokens2@example.com"} - results, err := database.GetUsersPushTokens(ctx, appSession, emails) - - assert.NoError(t, err) - assert.Len(t, results, 2) - assert.Equal(t, users[0].ExpoPushToken, results[0]["expoPushToken"]) - assert.Equal(t, users[1].ExpoPushToken, results[1]["expoPushToken"]) - }) - - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - mt.Run("Handle find error", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "find error", - })) - - // Call the function under test - appSession := &models.AppSession{ - DB: mt.Client, - } - - emails := []string{"TestGetUsersPushTokens1@example.com", "TestGetUsersPushTokens2@example.com"} - results, err := database.GetUsersPushTokens(ctx, appSession, emails) - - // Validate the result - assert.Error(t, err) - assert.Nil(t, results) - }) -} - -func TestAddNotification(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - // set gin run mode - gin.SetMode(configs.GetGinRunMode()) - ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - - mt.Run("nil database", func(mt *mtest.T) { - appSession := &models.AppSession{} - success, err := database.AddNotification(ctx, appSession, models.ScheduledNotification{}, false) - - assert.NotNil(t, err) - assert.False(t, success) - }) - - mt.Run("success add not push notification", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - appSession := &models.AppSession{ - DB: mt.Client, - } - success, err := database.AddNotification(ctx, appSession, models.ScheduledNotification{}, false) - - assert.NoError(t, err) - assert.True(t, success) - }) - - mt.Run("success add push notification", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - appSession := &models.AppSession{ - DB: mt.Client, - } - success, err := database.AddNotification(ctx, appSession, models.ScheduledNotification{}, true) - - assert.NoError(t, err) - assert.True(t, success) - }) + }) mt.Run("error", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "duplicate key error", - })) - - appSession := &models.AppSession{ - DB: mt.Client, - } - success, err := database.AddNotification(ctx, appSession, models.ScheduledNotification{}, false) - - assert.Error(t, err) - assert.False(t, success) - }) -} - -func TestGetScheduledNotifications(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - // set gin run mode - gin.SetMode(configs.GetGinRunMode()) - ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - - mt.Run("Database is nil", func(mt *mtest.T) { - // Call the function under test with a nil database - appSession := &models.AppSession{} - result, err := database.GetScheduledNotifications(ctx, appSession) - - // Validate the result - assert.Error(mt, err) - assert.Nil(mt, result) - assert.Equal(mt, "database is nil", err.Error()) - }) - - mt.Run("Retrieve scheduled notifications successfully", func(mt *mtest.T) { - // Add a mock response for a successful find - expectedNotifications := []models.ScheduledNotification{ - { - Title: "Test Notification", - Sent: false, - }, - { - Title: "Another Test Notification", - Sent: false, - }, - } - - firstBatch := mtest.CreateCursorResponse(1, "Notifications.mock", mtest.FirstBatch, bson.D{ - {Key: "title", Value: expectedNotifications[0].Title}, - {Key: "sent", Value: expectedNotifications[0].Sent}, - }) - getMoreBatch := mtest.CreateCursorResponse(0, "Notifications.mock", mtest.NextBatch, bson.D{ - {Key: "title", Value: expectedNotifications[1].Title}, - {Key: "sent", Value: expectedNotifications[1].Sent}, - }) - mt.AddMockResponses(firstBatch, getMoreBatch) - - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - } - - // Call the function under test - result, err := database.GetScheduledNotifications(ctx, appSession) - - // Validate the result - assert.NoError(mt, err) - assert.NotNil(mt, result) - assert.Equal(mt, expectedNotifications, result) - }) - - mt.Run("Find returns an error", func(mt *mtest.T) { - // Add a mock response that simulates a find error - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 1, - Message: "find error", - })) - - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - } - - // Call the function under test - result, err := database.GetScheduledNotifications(ctx, appSession) - - // Validate the result - assert.Error(mt, err) - assert.Nil(mt, result) - assert.Contains(mt, err.Error(), "find error") - }) -} - -func TestMarkNotificationAsSent(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - // set gin run mode - gin.SetMode(configs.GetGinRunMode()) - ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - - mt.Run("Database is nil", func(mt *mtest.T) { - // Call the function under test with a nil database - appSession := &models.AppSession{} - err := database.MarkNotificationAsSent(ctx, appSession, "60b725f10c9e9d63e5ecf26a") - - // Validate the result - assert.Error(mt, err) - assert.Equal(mt, "database is nil", err.Error()) - }) - - mt.Run("Invalid notification ID", func(mt *mtest.T) { - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - } - - // Call the function under test with an invalid notification ID - err := database.MarkNotificationAsSent(ctx, appSession, "invalid_id") - - // Validate the result - assert.Error(mt, err) - assert.Contains(mt, err.Error(), "the provided hex string is not a valid ObjectID") - }) - - mt.Run("Mark notification as sent successfully", func(mt *mtest.T) { - // Add a mock response for a successful update - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - } - - // Call the function under test - err := database.MarkNotificationAsSent(ctx, appSession, "60b725f10c9e9d63e5ecf26a") - - // Validate the result - assert.NoError(mt, err) - }) - - mt.Run("UpdateOne returns an error", func(mt *mtest.T) { - // Add a mock response that simulates an update error - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 1, - Message: "update error", - })) - - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - } - - // Call the function under test - err := database.MarkNotificationAsSent(ctx, appSession, "60b725f10c9e9d63e5ecf26a") - - // Validate the result - assert.Error(mt, err) - assert.Contains(mt, err.Error(), "update error") - }) -} - -func TestReadNotifications(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - // set gin run mode - gin.SetMode(configs.GetGinRunMode()) - ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - - mt.Run("Database is nil", func(mt *mtest.T) { - // Call the function under test with a nil database - appSession := &models.AppSession{} - err := database.ReadNotifications(ctx, appSession, "test@example.com") - - // Validate the result - assert.Error(mt, err) - assert.Equal(mt, "database is nil", err.Error()) - }) - - mt.Run("Update many notifications successfully", func(mt *mtest.T) { - // Add a mock response for a successful update - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - } - - // Call the function under test - err := database.ReadNotifications(ctx, appSession, "test@example.com") - - // Validate the result - assert.NoError(mt, err) - }) + email := "test@example.com" + token := "token123" - mt.Run("UpdateMany returns an error", func(mt *mtest.T) { - // Add a mock response that simulates an update error mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 1, - Message: "update error", + Code: 11000, + Message: "delete error", })) - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - } - - // Call the function under test - err := database.ReadNotifications(ctx, appSession, "test@example.com") + success, err := database.ClearResetToken(ctx, mt.Client, email, token) - // Validate the result - assert.Error(mt, err) - assert.Contains(mt, err.Error(), "update error") + assert.Error(t, err) + assert.False(t, success) }) } -func TestGetSecuritySettings(t *testing.T) { +func TestFilterUsersWithProjection(t *testing.T) { mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - // set gin run mode - gin.SetMode(configs.GetGinRunMode()) - ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - - mt.Run("Database is nil", func(mt *mtest.T) { - // Call the function under test with a nil database - appSession := &models.AppSession{} - result, err := database.GetSecuritySettings(ctx, appSession, "test@example.com") - - emptySettings := models.SecuritySettingsRequest{} - - // Validate the result - assert.Error(mt, err) - assert.Equal(mt, emptySettings, result) - assert.Equal(mt, "database is nil", err.Error()) - }) - - mt.Run("Retrieve security settings with mfa on and force logout off successfully", func(mt *mtest.T) { - // Add a mock response for a successful find - expectedSettings := models.SecuritySettingsRequest{ - Email: "test@example.com", - Mfa: "on", - ForceLogout: "off", - } - - firstBatch := mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: "test@example.com"}, - {Key: "security", Value: bson.D{ - {Key: "mfa", Value: true}, - {Key: "forceLogout", Value: false}, - }, - }, - }) - - mt.AddMockResponses(firstBatch) - - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - } - - // Call the function under test - result, err := database.GetSecuritySettings(ctx, appSession, "test@example.com") - - // Validate the result - assert.NoError(mt, err) - assert.NotNil(mt, result) - assert.Equal(mt, expectedSettings, result) - }) - - mt.Run("Retrieve security settings with mfa off and force logout off successfully", func(mt *mtest.T) { - // Add a mock response for a successful find - expectedSettings := models.SecuritySettingsRequest{ - Email: "test@example.com", - Mfa: "off", - ForceLogout: "off", - } - - firstBatch := mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: "test@example.com"}, - {Key: "security", Value: bson.D{ - {Key: "mfa", Value: false}, - {Key: "forceLogout", Value: false}, - }, - }, - }) - - mt.AddMockResponses(firstBatch) - - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - } - - // Call the function under test - result, err := database.GetSecuritySettings(ctx, appSession, "test@example.com") - - // Validate the result - assert.NoError(mt, err) - assert.NotNil(mt, result) - assert.Equal(mt, expectedSettings, result) - }) - - mt.Run("Retrieve security settings with mfa off and force logout on successfully", func(mt *mtest.T) { - // Add a mock response for a successful find - expectedSettings := models.SecuritySettingsRequest{ - Email: "test@example.com", - Mfa: "off", - ForceLogout: "on", - } - - firstBatch := mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: "test@example.com"}, - {Key: "security", Value: bson.D{ - {Key: "mfa", Value: false}, - {Key: "forceLogout", Value: true}, + tests := []struct { + name string + appSession *models.AppSession + filter primitive.M + projection bson.M + limit int64 + skip int64 + mockResponses []bson.D + expectedResult []bson.M + expectedCount int64 + expectedError error + }{ + { + name: "Database is nil", + appSession: &models.AppSession{ + DB: nil, }, + filter: primitive.M{"username": "testuser"}, + projection: bson.M{"username": 1, "email": 1}, + limit: 10, + skip: 0, + expectedResult: nil, + expectedCount: 0, + expectedError: errors.New("database is nil"), + }, + { + name: "Error in cursor all", + appSession: &models.AppSession{ + DB: mt.Client, }, - }) - - mt.AddMockResponses(firstBatch) + filter: primitive.M{"username": "testuser"}, + projection: bson.M{"username": 1, "email": 1}, + limit: 10, + skip: 0, + mockResponses: nil, + expectedResult: nil, + expectedCount: 0, + expectedError: errors.New("database is nil"), + }, + } - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt.Run("mock "+tt.name, func(mt *mtest.T) { + // Add mock responses + mt.AddMockResponses(tt.mockResponses...) - // Call the function under test - result, err := database.GetSecuritySettings(ctx, appSession, "test@example.com") + // Setup Gin context + gin.SetMode(configs.GetGinRunMode()) + ctx, _ := gin.CreateTestContext(nil) - // Validate the result - assert.NoError(mt, err) - assert.NotNil(mt, result) - assert.Equal(mt, expectedSettings, result) - }) + filter := models.FilterStruct{ + Filter: tt.filter, + Projection: tt.projection, + Limit: tt.limit, + Skip: tt.skip, + } - mt.Run("Retrieve security settings with mfa on and force logout on successfully", func(mt *mtest.T) { - // Add a mock response for a successful find - expectedSettings := models.SecuritySettingsRequest{ - Email: "test@example.com", - Mfa: "on", - ForceLogout: "on", - } + // Execute the function + results, count, err := database.FilterCollectionWithProjection(ctx, tt.appSession, "Users", filter) - firstBatch := mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: "test@example.com"}, - {Key: "security", Value: bson.D{ - {Key: "mfa", Value: true}, - {Key: "forceLogout", Value: true}, - }, - }, + // Validate results + if !reflect.DeepEqual(results, tt.expectedResult) { + t.Errorf("FilterUsersWithProjection() got = %v, want %v", results, tt.expectedResult) + } + if count != tt.expectedCount { + t.Errorf("FilterUsersWithProjection() count = %v, want %v", count, tt.expectedCount) + } + if err != nil && tt.expectedError != nil && err.Error() != tt.expectedError.Error() { + t.Errorf("FilterUsersWithProjection() error = %v, want %v", err, tt.expectedError) + } + if err != nil && tt.expectedError == nil { + t.Errorf("FilterUsersWithProjection() unexpected error = %v", err) + } + }) }) + } +} - mt.AddMockResponses(firstBatch) - - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - } - - // Call the function under test - result, err := database.GetSecuritySettings(ctx, appSession, "test@example.com") - - // Validate the result - assert.NoError(mt, err) - assert.NotNil(mt, result) - assert.Equal(mt, expectedSettings, result) - }) - - mt.Run("Retrieve security settings with mfa on and force logout off successfully from cache", func(mt *mtest.T) { - // Add a mock response for a successful find - expectedSettings := models.SecuritySettingsRequest{ - Email: "test@example.com", - Mfa: "on", - ForceLogout: "off", - } - - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - user := models.User{ - Email: "test@example.com", - Security: models.Security{ - MFA: true, - ForceLogout: false, - }, - } - - // add user to Cache - if userData, err := bson.Marshal(user); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(user.Email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(user.Email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) - - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } - - // Call the function under test - result, err := database.GetSecuritySettings(ctx, appSession, "test@example.com") - - // Validate the result - assert.NoError(mt, err) - assert.NotNil(mt, result) - assert.Equal(mt, expectedSettings, result) - }) - - mt.Run("Retrieve security settings with mfa off and force logout off successfully from cache", func(mt *mtest.T) { - // Add a mock response for a successful find - expectedSettings := models.SecuritySettingsRequest{ - Email: "test@example.com", - Mfa: "off", - ForceLogout: "off", - } - - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - user := models.User{ - Email: "test@example.com", - Security: models.Security{ - MFA: false, - ForceLogout: false, - }, - } - - // add user to Cache - if userData, err := bson.Marshal(user); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(user.Email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(user.Email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) +func TestFilterUsersWithProjectionSuccess(t *testing.T) { + email := "TestFilterUsersWithProjectionSuccess@example.com" + // Create database connection and cache + db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } + // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + w := httptest.NewRecorder() - // Call the function under test - result, err := database.GetSecuritySettings(ctx, appSession, "test@example.com") + // Create a response writer and context + ctx, _ := gin.CreateTestContext(w) - // Validate the result - assert.NoError(mt, err) - assert.NotNil(mt, result) - assert.Equal(mt, expectedSettings, result) - }) + // Create a new AppSession with the cache + appSession := &models.AppSession{ + DB: db, + } - mt.Run("Retrieve security settings with mfa off and force logout on successfully from cache", func(mt *mtest.T) { - // Add a mock response for a successful find - expectedSettings := models.SecuritySettingsRequest{ - Email: "test@example.com", - Mfa: "off", - ForceLogout: "on", - } + // Mock the DB response + collection := db.Database(configs.GetMongoDBName()).Collection("Users") - mt.AddMockResponses(mtest.CreateSuccessResponse()) + filter := primitive.M{"email": email} + projection := bson.M{"email": 1} + limit := 10 + skip := 0 - Cache := configs.CreateCache() + // Create test users + users := []models.UserDetails{ + { + Email: email, + }, + { + Email: email + ".za", + }, + { + Email: email + ".uk", + }, + { + Email: email + ".us", + }, + } - user := models.User{ - Email: "test@example.com", - Security: models.Security{ - MFA: false, - ForceLogout: true, - }, - } + // Insert test users into the database + for _, user := range users { + _, err := collection.InsertOne(ctx, user) - // add user to Cache - if userData, err := bson.Marshal(user); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(user.Email), userData); err != nil { - t.Fatal(err) - } + if err != nil { + t.Fatalf("Failed to insert test user into database: %v", err) } + } - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(user.Email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) - - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } + filter_arg := models.FilterStruct{ + Filter: filter, + Projection: projection, + Limit: int64(limit), + Skip: int64(skip), + } - // Call the function under test - result, err := database.GetSecuritySettings(ctx, appSession, "test@example.com") + // Execute the function + results, count, err := database.FilterCollectionWithProjection(ctx, appSession, "Users", filter_arg) - // Validate the result - assert.NoError(mt, err) - assert.NotNil(mt, result) - assert.Equal(mt, expectedSettings, result) - }) + // Validate results + if err != nil { + t.Fatalf("FilterUsersWithProjection() error = %v", err) + } - mt.Run("Retrieve security settings with mfa on and force logout on successfully from cache", func(mt *mtest.T) { - // Add a mock response for a successful find - expectedSettings := models.SecuritySettingsRequest{ - Email: "test@example.com", - Mfa: "on", - ForceLogout: "on", - } + if count != 1 { + t.Fatalf("FilterUsersWithProjection() count = %v, want %v", count, 1) + } - mt.AddMockResponses(mtest.CreateSuccessResponse()) + if len(results) != 1 { + t.Fatalf("FilterUsersWithProjection() results count = %v, want %v", len(results), 1) + } - Cache := configs.CreateCache() + if results[0]["email"] != email { + t.Fatalf("FilterUsersWithProjection() email = %v, want %v", results[0]["email"], email) + } +} - user := models.User{ - Email: "test@example.com", - Security: models.Security{ - MFA: true, - ForceLogout: true, - }, - } +func TestFilterUsersWithProjectionAndSortAscSuccess(t *testing.T) { + // Create database connection and cache + db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - // add user to Cache - if userData, err := bson.Marshal(user); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(user.Email), userData); err != nil { - t.Fatal(err) - } - } + // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + w := httptest.NewRecorder() - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(user.Email)) + // Create a response writer and context + ctx, _ := gin.CreateTestContext(w) - assert.Nil(t, err) - assert.NotNil(t, userA) + // Create a new AppSession with the cache + appSession := &models.AppSession{ + DB: db, + } - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } + // Mock the DB response + collection := db.Database(configs.GetMongoDBName()).Collection("Users") - // Call the function under test - result, err := database.GetSecuritySettings(ctx, appSession, "test@example.com") + filter := primitive.M{ + "email": primitive.Regex{ + Pattern: "^" + "TestFilterUsersWithProjectionAndSortAscSuccess", // "^" ensures the pattern matches the beginning of the string + Options: "i", // "i" makes the regex case-insensitive (optional) + }, + } + projection := bson.M{"email": 1} + limit := 10 + skip := 0 + sort := bson.M{"email": 1} - // Validate the result - assert.NoError(mt, err) - assert.NotNil(mt, result) - assert.Equal(mt, expectedSettings, result) - }) + // Create test users + users := []models.UserDetails{ + { + Email: "TestFilterUsersWithProjectionAndSortAscSuccess3@example.com", + }, + { + Email: "TestFilterUsersWithProjectionAndSortAscSuccess2@example.com", + }, + { + Email: "TestFilterUsersWithProjectionAndSortAscSuccess1@example.com", + }, + { + Email: "TestFilterUsersWithProjectionAndSortAscSuccess4@example.com", + }, + } - mt.Run("Find returns an error", func(mt *mtest.T) { - // Add a mock response that simulates a find error - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 1, - Message: "find error", - })) + // Insert test users into the database + for _, user := range users { + _, err := collection.InsertOne(ctx, user) - // Initialize the app session with the mock client - appSession := &models.AppSession{ - DB: mt.Client, + if err != nil { + t.Fatalf("Failed to insert test user into database: %v", err) } + } - emptySettings := models.SecuritySettingsRequest{} + filter_arg := models.FilterStruct{ + Filter: filter, + Projection: projection, + Limit: int64(limit), + Skip: int64(skip), + Sort: sort, + } - // Call the function under test - result, err := database.GetSecuritySettings(ctx, appSession, "test@example.com") + // Execute the function + results, count, err := database.FilterCollectionWithProjection(ctx, appSession, "Users", filter_arg) - // Validate the result - assert.Error(mt, err) - assert.Equal(mt, emptySettings, result) - assert.Contains(mt, err.Error(), "find error") - }) -} + // Validate results + if err != nil { + t.Fatalf("FilterUsersWithProjection() error = %v", err) + } -func TestUpdateSecuritySettings(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + if count != 4 { + t.Fatalf("FilterUsersWithProjection() count = %v, want %v", count, 1) + } - // set gin run mode - gin.SetMode(configs.GetGinRunMode()) - ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + if len(results) != 4 { + t.Fatalf("FilterUsersWithProjection() results count = %v, want %v", len(results), 1) + } - mt.Run("Nil database", func(mt *mtest.T) { - // Call the function under test - appsession := &models.AppSession{} - err := database.UpdateSecuritySettings(ctx, appsession, models.SecuritySettingsRequest{}) + if results[0]["email"] != "TestFilterUsersWithProjectionAndSortAscSuccess1@example.com" { + t.Fatalf("FilterUsersWithProjection() email = %v, want %v", results[0]["email"], "TestFilterUsersWithProjectionAndSortAscSuccess1@example.com") + } +} - // Validate the result - assert.Error(t, err) - }) +func TestFilterUsersWithProjectionAndSortDescSuccess(t *testing.T) { + // Create database connection and cache + db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - mt.Run("Test update password successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) + // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + w := httptest.NewRecorder() - security := models.SecuritySettingsRequest{ - Email: "test@example.com", - NewPassword: "blah-blah", - } + // Create a response writer and context + ctx, _ := gin.CreateTestContext(w) - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } + // Create a new AppSession with the cache + appSession := &models.AppSession{ + DB: db, + } - err := database.UpdateSecuritySettings(ctx, appsession, security) + // Mock the DB response + collection := db.Database(configs.GetMongoDBName()).Collection("Users") - // Validate the result - assert.NoError(t, err) - }) + filter := primitive.M{ + "email": primitive.Regex{ + Pattern: "^" + "TestFilterUsersWithProjectionAndSortDescSuccess", // "^" ensures the pattern matches the beginning of the string + Options: "i", // "i" makes the regex case-insensitive (optional) + }, + } + projection := bson.M{"email": 1} + limit := 10 + skip := 0 + sort := bson.M{"email": -1} - mt.Run("Test set mfa on successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) + // Create test users + users := []models.UserDetails{ + { + Email: "TestFilterUsersWithProjectionAndSortDescSuccess3@example.com", + }, + { + Email: "TestFilterUsersWithProjectionAndSortDescSuccess2@example.com", + }, + { + Email: "TestFilterUsersWithProjectionAndSortDescSuccess1@example.com", + }, + { + Email: "TestFilterUsersWithProjectionAndSortDescSuccess4@example.com", + }, + } - security := models.SecuritySettingsRequest{ - Email: "test@example.com", - Mfa: "on", - } + // Insert test users into the database + for _, user := range users { + _, err := collection.InsertOne(ctx, user) - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, + if err != nil { + t.Fatalf("Failed to insert test user into database: %v", err) } + } - err := database.UpdateSecuritySettings(ctx, appsession, security) - - // Validate the result - assert.NoError(t, err) - }) + filter_arg := models.FilterStruct{ + Filter: filter, + Projection: projection, + Limit: int64(limit), + Skip: int64(skip), + Sort: sort, + } - mt.Run("Test set mfa off successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) + // Execute the function + results, count, err := database.FilterCollectionWithProjection(ctx, appSession, "Users", filter_arg) - security := models.SecuritySettingsRequest{ - Email: "test@example.com", - Mfa: "off", - } + // Validate results + if err != nil { + t.Fatalf("FilterUsersWithProjection() error = %v", err) + } - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } + if count != 4 { + t.Fatalf("FilterUsersWithProjection() count = %v, want %v", count, 1) + } - err := database.UpdateSecuritySettings(ctx, appsession, security) + if len(results) != 4 { + t.Fatalf("FilterUsersWithProjection() results count = %v, want %v", len(results), 1) + } - // Validate the result - assert.NoError(t, err) - }) + if results[0]["email"] != "TestFilterUsersWithProjectionAndSortDescSuccess4@example.com" { + t.Fatalf("FilterUsersWithProjection() email = %v, want %v", results[0]["email"], "TestFilterUsersWithProjectionAndSortDescSuccess4@example.com") + } +} - mt.Run("Test set force logout on successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) +func TestCheckIfUserIsLoggingInFromKnownLocation(t *testing.T) { + // Setup mock MongoDB instance + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - security := models.SecuritySettingsRequest{ - Email: "test@example.com", - ForceLogout: "on", - } + gin.SetMode(configs.GetGinRunMode()) - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } + // Create a new HTTP request with the POST method. + req, _ := http.NewRequest("POST", "/", nil) - err := database.UpdateSecuritySettings(ctx, appsession, security) + // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + w := httptest.NewRecorder() - // Validate the result - assert.NoError(t, err) - }) + // Create a new context with the Request and ResponseWriter. + ctx, _ := gin.CreateTestContext(w) + ctx.Request = req - mt.Run("Test set force logout off successfully", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) + // Optionally, set any values in the context. + ctx.Set("test", "test") - security := models.SecuritySettingsRequest{ - Email: "test@example.com", - ForceLogout: "off", - } + email := "test@example.com" + mt.Run("Nil database", func(mt *mtest.T) { // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } - - err := database.UpdateSecuritySettings(ctx, appsession, security) + appsession := &models.AppSession{} + yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) // Validate the result - assert.NoError(t, err) + assert.Error(t, err) + assert.False(t, yes) + assert.Nil(t, info) }) - mt.Run("Test set password successfully in cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - user := models.User{ - Email: "test@example.com", - Password: "blah-blah", - } - - // add user to Cache - if userData, err := bson.Marshal(user); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(user.Email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(user.Email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) - - security := models.SecuritySettingsRequest{ - Email: "test@example.com", - NewPassword: "blah-blah-blah", - } + mt.Run("Location does not exist", func(mt *mtest.T) { + mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ + {Key: "email", Value: email}, + })) // Call the function under test appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, + DB: mt.Client, } - - err = database.UpdateSecuritySettings(ctx, appsession, security) + yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) // Validate the result assert.NoError(t, err) - - // Assert that the user is in the Cache - userA, err = Cache.Get(cache.UserKey(user.Email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) - - // unmarshal user - var userB models.User - if err := bson.Unmarshal(userA, &userB); err != nil { - t.Fatal(err) - } - - assert.Equal(t, "blah-blah-blah", userB.Password) + assert.False(t, yes) + assert.NotNil(t, info) + assert.Equal(t, "Cape Town", info.City) + assert.Equal(t, "Western Cape", info.Region) + assert.Equal(t, "South Africa", info.Country) }) - mt.Run("Test set mfa on successfully in cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - Cache := configs.CreateCache() - - user := models.User{ - Email: "test@example.com", - Security: models.Security{ - MFA: false, - ForceLogout: false, - }, - } - - // add user to Cache - if userData, err := bson.Marshal(user); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(user.Email), userData); err != nil { - t.Fatal(err) - } - } - - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(user.Email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) - - security := models.SecuritySettingsRequest{ - Email: "test@example.com", - Mfa: "on", - } + mt.Run("Location exists and is valid", func(mt *mtest.T) { + mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ + {Key: "email", Value: email}, + {Key: "locations", Value: bson.A{ + bson.D{ + {Key: "city", Value: "Cape Town"}, + {Key: "region", Value: "Western Cape"}, + {Key: "country", Value: "South Africa"}, + }, + }}, + })) // Call the function under test appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, + DB: mt.Client, } - - err = database.UpdateSecuritySettings(ctx, appsession, security) + yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) // Validate the result assert.NoError(t, err) + assert.True(t, yes) + assert.Nil(t, info) + }) - // Assert that the user is in the Cache - userA, err = Cache.Get(cache.UserKey(user.Email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) + mt.Run("Location exists but ip address unkwown", func(mt *mtest.T) { + mt.AddMockResponses(mtest.CreateCursorResponse(1, configs.GetMongoDBName()+".Users", mtest.FirstBatch, bson.D{ + {Key: "email", Value: email}, + {Key: "locations", Value: bson.A{ + bson.D{ + {Key: "city", Value: "Durban"}, + {Key: "region", Value: "KwaZulu-Natal"}, + {Key: "country", Value: "South Africa"}, + }, + }}, + })) - // unmarshal user - var userB models.User - if err := bson.Unmarshal(userA, &userB); err != nil { - t.Fatal(err) + // Call the function under test + appsession := &models.AppSession{ + DB: mt.Client, } + yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) - assert.Equal(t, true, userB.Security.MFA) + // Validate the result + assert.NoError(t, err) + assert.False(t, yes) + assert.NotNil(t, info) + assert.Equal(t, "Cape Town", info.City) + assert.Equal(t, "Western Cape", info.Region) + assert.Equal(t, "South Africa", info.Country) }) - mt.Run("Test set mfa off successfully in cache", func(mt *mtest.T) { + mt.Run("Location exists and is valid in cache", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) - Cache := configs.CreateCache() + cache := configs.CreateCache() - user := models.User{ - Email: "test@example.com", - Security: models.Security{ - MFA: true, - ForceLogout: false, + userStruct := models.User{ + Email: email, + KnownLocations: []models.Location{ + { + City: "Cape Town", + Region: "Western Cape", + Country: "South Africa", + }, }, } - // add user to Cache - if userData, err := bson.Marshal(user); err != nil { + // add userstruct to cache + if userData, err := bson.Marshal(userStruct); err != nil { t.Fatal(err) } else { - if err := Cache.Set(cache.UserKey(user.Email), userData); err != nil { + if err := cache.Set(email, userData); err != nil { t.Fatal(err) } } - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(user.Email)) + // Assert that the user is in the cache + userA, err := cache.Get(email) assert.Nil(t, err) assert.NotNil(t, userA) - security := models.SecuritySettingsRequest{ - Email: "test@example.com", - Mfa: "off", - } - - // Call the function under test appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } - err = database.UpdateSecuritySettings(ctx, appsession, security) + // Call the function under test + yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) // Validate the result assert.NoError(t, err) - - // Assert that the user is in the Cache - userA, err = Cache.Get(cache.UserKey(user.Email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) - - // unmarshal user - var userB models.User - if err := bson.Unmarshal(userA, &userB); err != nil { - t.Fatal(err) - } - - assert.Equal(t, false, userB.Security.MFA) + assert.True(t, yes) + assert.Nil(t, info) }) - mt.Run("Test set force logout on successfully in cache", func(mt *mtest.T) { + mt.Run("Location exists but does not match what is in cache", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) - Cache := configs.CreateCache() + cache := configs.CreateCache() - user := models.User{ - Email: "test@example.com", - Security: models.Security{ - MFA: false, - ForceLogout: false, + userStruct := models.User{ + Email: email, + KnownLocations: []models.Location{ + { + City: "Durban", + Region: "KwaZulu-Natal", + Country: "South Africa", + }, }, } - // add user to Cache - if userData, err := bson.Marshal(user); err != nil { + // add userstruct to cache + if userData, err := bson.Marshal(userStruct); err != nil { t.Fatal(err) } else { - if err := Cache.Set(cache.UserKey(user.Email), userData); err != nil { + if err := cache.Set(email, userData); err != nil { t.Fatal(err) } } - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(user.Email)) + // Assert that the user is in the cache + userA, err := cache.Get(email) assert.Nil(t, err) assert.NotNil(t, userA) - security := models.SecuritySettingsRequest{ - Email: "test@example.com", - ForceLogout: "on", - } - - // Call the function under test appsession := &models.AppSession{ DB: mt.Client, - Cache: Cache, + Cache: cache, } - err = database.UpdateSecuritySettings(ctx, appsession, security) + // Call the function under test + yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) // Validate the result assert.NoError(t, err) + assert.False(t, yes) + assert.NotNil(t, info) + assert.Equal(t, "Cape Town", info.City) + assert.Equal(t, "Western Cape", info.Region) + assert.Equal(t, "South Africa", info.Country) + }) - // Assert that the user is in the Cache - userA, err = Cache.Get(cache.UserKey(user.Email)) - - assert.Nil(t, err) - assert.NotNil(t, userA) + mt.Run("Handle find error", func(mt *mtest.T) { + mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ + Code: 11000, + Message: "find error", + })) - // unmarshal user - var userB models.User - if err := bson.Unmarshal(userA, &userB); err != nil { - t.Fatal(err) - } + // Call the function under test + appsession := &models.AppSession{} + yes, info, err := database.CheckIfUserIsLoggingInFromKnownLocation(ctx, appsession, email, ctx.ClientIP()) - assert.Equal(t, true, userB.Security.ForceLogout) + // Validate the result + assert.Error(t, err) + assert.False(t, yes) + assert.Nil(t, info) }) +} - mt.Run("Test set force logout off successfully in cache", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) +func TestGetUsersPushTokens(t *testing.T) { + users := []models.User{ + { + Email: "TestGetUsersPushTokens1@example.com", + ExpoPushToken: "b1b2b3b4b5b6b7b8b9b0", + Notifications: models.Notifications{ + Invites: true, + }, + }, + { + Email: "TestGetUsersPushTokens2@example.com", + ExpoPushToken: "a1a2a3a4a5a6a7a8a9a0", + Notifications: models.Notifications{ + Invites: true, + }, + }, + } + // Create database connection and cache + db := configs.ConnectToDatabase(constants.AdminDBAccessOption) - Cache := configs.CreateCache() + // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + w := httptest.NewRecorder() - user := models.User{ - Email: "test@example.com", - Security: models.Security{ - MFA: false, - ForceLogout: true, - }, - } + // Create a response writer and context + ctx, _ := gin.CreateTestContext(w) - // add user to Cache - if userData, err := bson.Marshal(user); err != nil { - t.Fatal(err) - } else { - if err := Cache.Set(cache.UserKey(user.Email), userData); err != nil { - t.Fatal(err) - } - } + // Create a new AppSession with the cache + appSession := &models.AppSession{ + DB: db, + } - // Assert that the user is in the Cache - userA, err := Cache.Get(cache.UserKey(user.Email)) + // Mock the DB response + collection := db.Database(configs.GetMongoDBName()).Collection("Users") - assert.Nil(t, err) - assert.NotNil(t, userA) + // Insert test users into the database + for _, user := range users { + _, err := collection.InsertOne(ctx, user) - security := models.SecuritySettingsRequest{ - Email: "test@example.com", - ForceLogout: "off", + if err != nil { + t.Fatalf("Failed to insert test user into database: %v", err) } + } - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - Cache: Cache, - } + // Test case: Database is nil + t.Run("Database is nil", func(t *testing.T) { + emails := []string{"test@example.com"} + + appsession := &models.AppSession{} + results, err := database.GetUsersPushTokens(ctx, appsession, emails) + assert.Nil(t, results) + assert.EqualError(t, err, "database is nil") + }) - err = database.UpdateSecuritySettings(ctx, appsession, security) + // Test case: Empty emails + t.Run("Empty emails", func(t *testing.T) { + emails := []string{} + results, err := database.GetUsersPushTokens(ctx, appSession, emails) - // Validate the result - assert.NoError(t, err) + assert.Nil(t, results) + assert.NotNil(t, err) + assert.EqualError(t, err, "no emails provided") + }) - // Assert that the user is in the Cache - userA, err = Cache.Get(cache.UserKey(user.Email)) + // Test case: No matching users + t.Run("No matching users", func(t *testing.T) { + emails := []string{"TestGetUsersPushTokens3@example.com"} + results, err := database.GetUsersPushTokens(ctx, appSession, emails) + assert.Nil(t, results) assert.Nil(t, err) - assert.NotNil(t, userA) + }) - // unmarshal user - var userB models.User - if err := bson.Unmarshal(userA, &userB); err != nil { - t.Fatal(err) - } + // Test case: Successful query with one user + t.Run("Successful query with one user a", func(t *testing.T) { + emails := []string{"TestGetUsersPushTokens1@example.com"} + results, err := database.GetUsersPushTokens(ctx, appSession, emails) - assert.Equal(t, false, userB.Security.ForceLogout) + assert.NoError(t, err) + assert.Len(t, results, 1) + assert.Equal(t, users[0].ExpoPushToken, results[0]["expoPushToken"]) }) - mt.Run("Update Error", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "update error", - })) - - security := models.SecuritySettingsRequest{ - Email: "test@example.com", - } + t.Run("Successful query with one user b", func(t *testing.T) { + emails := []string{"TestGetUsersPushTokens2@example.com"} + results, err := database.GetUsersPushTokens(ctx, appSession, emails) - // Call the function under test - appsession := &models.AppSession{ - DB: mt.Client, - } + assert.NoError(t, err) + assert.Len(t, results, 1) + assert.Equal(t, users[1].ExpoPushToken, results[0]["expoPushToken"]) + }) - err := database.UpdateSecuritySettings(ctx, appsession, security) + // Test case: Successful query with two users + t.Run("Successful query with two users", func(t *testing.T) { + emails := []string{"TestGetUsersPushTokens1@example.com", "TestGetUsersPushTokens2@example.com"} + results, err := database.GetUsersPushTokens(ctx, appSession, emails) - // Validate the result - assert.Error(t, err) + assert.NoError(t, err) + assert.Len(t, results, 2) + assert.Equal(t, users[0].ExpoPushToken, results[0]["expoPushToken"]) + assert.Equal(t, users[1].ExpoPushToken, results[1]["expoPushToken"]) }) } diff --git a/occupi-backend/tests/middleware_test.go b/occupi-backend/tests/middleware_test.go index ec067ac9..651d43be 100644 --- a/occupi-backend/tests/middleware_test.go +++ b/occupi-backend/tests/middleware_test.go @@ -847,12 +847,12 @@ func TestAttachOTPRateLimitMiddleware(t *testing.T) { clientIP: "192.168.0.1", waitDuration: 4 * time.Second, expectedCode: http.StatusOK, - expectedBody: "OTP request successful", + expectedBody: `OTP request successfu`, }, { description: "request after a couple seconds should succeed", clientIP: "192.168.0.1", - waitDuration: 4 * time.Second, + waitDuration: 5 * time.Second, expectedCode: http.StatusOK, expectedBody: "OTP request successful", }, diff --git a/occupi-backend/tests/utils_test.go b/occupi-backend/tests/utils_test.go index e117567b..9d1f123e 100644 --- a/occupi-backend/tests/utils_test.go +++ b/occupi-backend/tests/utils_test.go @@ -1530,14 +1530,14 @@ func TestSantizeProjection(t *testing.T) { input: models.QueryInput{ Projection: []string{"username", "emails", "age"}, }, - expected: []string{"username", "emails", "age"}, + expected: []string{"username", "age"}, }, { name: "Projection with Password, UnsentExpoPushTokens, and Emails", input: models.QueryInput{ Projection: []string{"username", "password", "unsentExpoPushTokens", "emails", "age"}, }, - expected: []string{"username", "emails", "age"}, + expected: []string{"username", "age"}, }, } @@ -1565,6 +1565,7 @@ func TestConstructProjection(t *testing.T) { expected: bson.M{ "password": 0, "unsentExpoPushTokens": 0, + "emails": 0, "_id": 0, }, }, @@ -1609,10 +1610,9 @@ func TestConstructProjection(t *testing.T) { queryInput: models.QueryInput{ Projection: []string{"username", "emails", "age"}, }, - sanitizedProjection: []string{"username", "emails", "age"}, + sanitizedProjection: []string{"username", "age"}, expected: bson.M{ "username": 1, - "emails": 1, "age": 1, "_id": 0, }, @@ -2260,13 +2260,6 @@ func TestGetClaimsFromCTX(t *testing.T) { assert.Nil(t, err) } - // check that originToken has been set properly in context - if tt.tokenCookie != "" { - assert.Equal(t, "cookie", c.GetString("tokenOrigin")) - } else if tt.tokenHeader != "" { - assert.Equal(t, "header", c.GetString("tokenOrigin")) - } - // Check the expected claims assert.Equal(t, tt.expectedClaims, returnedclaims) }) diff --git a/python-code/attendance_scaler.pkl b/python-code/attendance_scaler.pkl deleted file mode 100644 index 11f20c27..00000000 Binary files a/python-code/attendance_scaler.pkl and /dev/null differ diff --git a/python-code/requirements.txt b/python-code/requirements.txt deleted file mode 100644 index 93265525..00000000 --- a/python-code/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -Flask -joblib -numpy -pandas -requests -scikit-learn -seaborn -matplotlib -gunicorn diff --git a/python-code/scaler.py b/python-code/scaler.py deleted file mode 100644 index 58f21da7..00000000 --- a/python-code/scaler.py +++ /dev/null @@ -1,8 +0,0 @@ -# scaler.py -import numpy as np -from sklearn.preprocessing import StandardScaler - -# Initialize the scaler (use the same scaler used during training) -# Assume you have access to the original training data for initializing the scaler -X_train = np.array([[0, 3, 15, 0, 1], [1, 3, 16, 0, 1], [2, 3, 17, 0, 1], [3, 3, 18, 0, 1], [4, 3, 19, 0, 1], [5, 3, 20, 1, 1], [6, 3, 21, 1, 1]]) -scaler = StandardScaler().fit(X_train)