diff --git a/.github/workflows/web-quality.yml b/.github/workflows/web-quality.yml new file mode 100644 index 00000000..7280b333 --- /dev/null +++ b/.github/workflows/web-quality.yml @@ -0,0 +1,59 @@ +name: Web Quality + +on: + pull_request: + branches: + - dev + - main + push: + branches: + - dev + - main + +jobs: + web-quality: + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/web + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + run_install: false + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + cache-dependency-path: apps/web/pnpm-lock.yaml + + - name: Setup Firebase CLI + working-directory: . + run: npm install -g firebase-tools + + - name: Generate Data Connect SDK + working-directory: . + run: | + cp backend/dataconnect/dataconnect.dev.yaml backend/dataconnect/dataconnect.yaml + firebase dataconnect:sdk:generate --non-interactive + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Typecheck + run: pnpm typecheck + + - name: Test + run: pnpm test + + - name: Build + run: pnpm build diff --git a/Makefile b/Makefile index a54c9e9d..2b2f8c55 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,7 @@ help: @echo " make web-dev Start local web frontend dev server" @echo " make web-build [ENV=dev] Build web frontend for production (dev/staging)" @echo " make web-lint Run linter for web frontend" + @echo " make web-test Run tests for web frontend" @echo " make web-preview Preview web frontend build" @echo " make web-deploy [ENV=dev] Build and deploy web app (dev/staging)" @echo "" @@ -50,6 +51,8 @@ help: @echo " make mobile-client-build PLATFORM=apk Build client app (apk/ipa/etc)" @echo " make mobile-staff-dev-android [DEVICE=android] Run staff app (Android)" @echo " make mobile-staff-build PLATFORM=apk Build staff app (apk/ipa/etc)" + @echo " make mobile-analyze Run flutter analyze for client+staff" + @echo " make mobile-test Run flutter test for client+staff" @echo " make mobile-hot-reload Hot reload running Flutter app" @echo " make mobile-hot-restart Hot restart running Flutter app" @echo "" @@ -83,4 +86,4 @@ help: @echo " 💡 Tip: Run 'make mobile-install' first for mobile development" @echo " 💡 Tip: Use 'make dataconnect-sync' after schema changes" @echo " 💡 Tip: Default ENV=dev, use ENV=staging for staging environment" - @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" \ No newline at end of file + @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/apps/mobile/README.md b/apps/mobile/README.md index 77d948ad..2b6c2076 100644 --- a/apps/mobile/README.md +++ b/apps/mobile/README.md @@ -71,6 +71,7 @@ make mobile-staff-dev-android DEVICE= - **Bootstrap**: `melos bootstrap` (Installs all dependencies) - **Generate All**: `melos run gen:all` (Localization + Code Generation) - **Analyze**: `melos run analyze:all` +- **Test**: `melos run test:all` - **Help**: `melos run info` (Shows all available custom scripts) ## 🏗 Coding Principles diff --git a/apps/mobile/apps/client/test/smoke_test.dart b/apps/mobile/apps/client/test/smoke_test.dart new file mode 100644 index 00000000..75abe0ce --- /dev/null +++ b/apps/mobile/apps/client/test/smoke_test.dart @@ -0,0 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('client smoke test', () { + expect(2 + 2, 4); + }); +} diff --git a/apps/mobile/apps/staff/test/smoke_test.dart b/apps/mobile/apps/staff/test/smoke_test.dart new file mode 100644 index 00000000..106e8a8c --- /dev/null +++ b/apps/mobile/apps/staff/test/smoke_test.dart @@ -0,0 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('staff smoke test', () { + expect(2 + 2, 4); + }); +} diff --git a/apps/mobile/pubspec.yaml b/apps/mobile/pubspec.yaml index 83f170e7..f1380d0c 100644 --- a/apps/mobile/pubspec.yaml +++ b/apps/mobile/pubspec.yaml @@ -78,6 +78,18 @@ melos: packageFilters: dependsOn: build_runner + analyze:all: + run: | + melos exec --scope="krowwithus_client" -- "flutter analyze" + melos exec --scope="krowwithus_staff" -- "flutter analyze" + description: "Run flutter analyze for both client and staff apps." + + test:all: + run: | + melos exec --scope="krowwithus_client" -- "flutter test" + melos exec --scope="krowwithus_staff" -- "flutter test" + description: "Run flutter tests for both client and staff apps." + build:client: run: | melos run gen:l10n --filter="core_localization" diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js index 5e6b472f..c8c10cfa 100644 --- a/apps/web/eslint.config.js +++ b/apps/web/eslint.config.js @@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(['dist', 'src/dataconnect-generated/**']), { files: ['**/*.{ts,tsx}'], extends: [ @@ -19,5 +19,24 @@ export default defineConfig([ ecmaVersion: 2020, globals: globals.browser, }, + rules: { + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + '@typescript-eslint/no-empty-object-type': 'warn', + '@typescript-eslint/no-require-imports': 'warn', + 'react-refresh/only-export-components': 'warn', + 'react-hooks/exhaustive-deps': 'warn', + 'react-hooks/set-state-in-effect': 'warn', + 'react-hooks/purity': 'warn', + 'react-hooks/preserve-manual-memoization': 'warn', + }, }, ]) diff --git a/apps/web/package.json b/apps/web/package.json index cfbdab04..8e35cd96 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,8 +5,11 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && vite build", + "build": "vite build", + "typecheck": "tsc -b", "lint": "eslint .", + "test": "vitest run", + "test:watch": "vitest", "preview": "vite preview" }, "dependencies": { @@ -31,7 +34,6 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "dataconnect-generated": "link:C:/Users/dell/AppData/Local/pnpm/store/5/node_modules/src/dataconnect-generated", "date-fns": "^4.1.0", "firebase": "^12.8.0", "framer-motion": "^12.29.2", @@ -67,6 +69,7 @@ "tailwindcss": "^4.1.18", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "vite": "^7.2.4", + "vitest": "^3.2.4" } } diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 4b1de6e6..6f3eca62 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -5,7 +5,6 @@ settings: excludeLinksFromLockfile: false overrides: - dataconnect-generated: link:../../../../../AppData/Local/pnpm/global/5/node_modules/src/dataconnect-generated '@dataconnect/generated': link:src/dataconnect-generated importers: @@ -75,9 +74,6 @@ importers: cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - dataconnect-generated: - specifier: link:../../../../../AppData/Local/pnpm/global/5/node_modules/src/dataconnect-generated - version: link:../../../../../AppData/Local/pnpm/global/5/node_modules/src/dataconnect-generated date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -181,6 +177,9 @@ importers: vite: specifier: ^7.2.4 version: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2) packages: @@ -1787,6 +1786,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -1814,6 +1816,9 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1899,6 +1904,35 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1927,6 +1961,10 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1958,6 +1996,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1969,10 +2011,18 @@ packages: caniuse-lite@1.0.30001766: resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -2089,6 +2139,10 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2125,6 +2179,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2198,6 +2255,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2205,6 +2265,10 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2415,6 +2479,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -2528,6 +2595,9 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2611,6 +2681,13 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2827,10 +2904,19 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2843,6 +2929,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2868,10 +2957,28 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -2944,6 +3051,11 @@ packages: victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2984,6 +3096,34 @@ packages: yaml: optional: true + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -3010,6 +3150,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4698,6 +4843,11 @@ snapshots: dependencies: '@babel/types': 7.28.6 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -4722,6 +4872,8 @@ snapshots: '@types/d3-timer@3.0.2': {} + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -4843,6 +4995,48 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -4868,6 +5062,8 @@ snapshots: dependencies: tslib: 2.8.1 + assertion-error@2.0.1: {} + asynckit@0.4.0: {} autoprefixer@10.4.23(postcss@8.5.6): @@ -4908,6 +5104,8 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -4917,11 +5115,21 @@ snapshots: caniuse-lite@1.0.30001766: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + check-error@2.1.3: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -5030,6 +5238,8 @@ snapshots: decimal.js-light@2.5.1: {} + deep-eql@5.0.2: {} + deep-is@0.1.4: {} delayed-stream@1.0.0: {} @@ -5057,6 +5267,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -5184,10 +5396,16 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} eventemitter3@5.0.4: {} + expect-type@1.3.0: {} + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -5386,6 +5604,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -5468,6 +5688,8 @@ snapshots: long@5.3.2: {} + loupe@3.2.1: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -5539,6 +5761,10 @@ snapshots: path-key@3.1.1: {} + pathe@2.0.3: {} + + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@4.0.3: {} @@ -5807,8 +6033,14 @@ snapshots: shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + source-map-js@1.2.1: {} + stackback@0.0.2: {} + + std-env@3.10.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5821,6 +6053,10 @@ snapshots: strip-json-comments@3.1.1: {} + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -5839,11 +6075,21 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + tr46@0.0.3: {} ts-api-utils@2.4.0(typescript@5.9.3): @@ -5919,6 +6165,27 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite-node@3.2.4(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2): dependencies: esbuild: 0.27.2 @@ -5933,6 +6200,47 @@ snapshots: jiti: 2.6.1 lightningcss: 1.30.2 + vitest@3.2.4(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2) + vite-node: 3.2.4(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.10.9 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + void-elements@3.1.0: {} web-vitals@4.2.4: {} @@ -5956,6 +6264,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: diff --git a/apps/web/pnpm-workspace.yaml b/apps/web/pnpm-workspace.yaml index 13edf779..117a15bd 100644 --- a/apps/web/pnpm-workspace.yaml +++ b/apps/web/pnpm-workspace.yaml @@ -1,3 +1,2 @@ overrides: '@dataconnect/generated': link:src/dataconnect-generated - dataconnect-generated: link:../../../../../AppData/Local/pnpm/global/5/node_modules/src/dataconnect-generated diff --git a/apps/web/src/features/auth/roleUtils.test.ts b/apps/web/src/features/auth/roleUtils.test.ts new file mode 100644 index 00000000..65069e29 --- /dev/null +++ b/apps/web/src/features/auth/roleUtils.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest' +import { getDashboardPath, normalizeRole } from './roleUtils' + +describe('roleUtils', () => { + it('normalizes lowercase and uppercase roles', () => { + expect(normalizeRole('admin')).toBe('admin') + expect(normalizeRole('CLIENT')).toBe('client') + expect(normalizeRole('Vendor')).toBe('vendor') + }) + + it('returns null for unsupported roles', () => { + expect(normalizeRole('super_admin')).toBeNull() + expect(normalizeRole('')).toBeNull() + }) + + it('maps known roles to the proper dashboard path', () => { + expect(getDashboardPath('admin')).toBe('/dashboard/admin') + expect(getDashboardPath('CLIENT')).toBe('/dashboard/client') + expect(getDashboardPath('vendor')).toBe('/dashboard/vendor') + }) + + it('falls back to client dashboard for unknown roles', () => { + expect(getDashboardPath('unknown')).toBe('/dashboard/client') + }) +}) diff --git a/apps/web/src/features/auth/roleUtils.ts b/apps/web/src/features/auth/roleUtils.ts new file mode 100644 index 00000000..74375573 --- /dev/null +++ b/apps/web/src/features/auth/roleUtils.ts @@ -0,0 +1,30 @@ +export type Role = 'admin' | 'client' | 'vendor' +export type RawRole = Role | Uppercase | string + +const DEFAULT_DASHBOARD_PATH = '/dashboard/client' + +const ROLE_TO_DASHBOARD: Record = { + admin: '/dashboard/admin', + client: '/dashboard/client', + vendor: '/dashboard/vendor', +} + +export const normalizeRole = (role: RawRole): Role | null => { + const normalizedRole = role.toLowerCase() + + if (normalizedRole === 'admin' || normalizedRole === 'client' || normalizedRole === 'vendor') { + return normalizedRole + } + + return null +} + +export const getDashboardPath = (role: RawRole): string => { + const normalizedRole = normalizeRole(role) + + if (!normalizedRole) { + return DEFAULT_DASHBOARD_PATH + } + + return ROLE_TO_DASHBOARD[normalizedRole] +} diff --git a/apps/web/src/features/business/clients/AddClient.tsx b/apps/web/src/features/business/clients/AddClient.tsx index 9313b9f6..57144007 100644 --- a/apps/web/src/features/business/clients/AddClient.tsx +++ b/apps/web/src/features/business/clients/AddClient.tsx @@ -56,7 +56,7 @@ export default function AddClient() { const { mutateAsync: createBusiness, isPending: isCreatingBusiness } = useCreateBusiness(dataConnect); const { mutateAsync: createHub, isPending: isCreatingHub } = useCreateTeamHub(dataConnect); - const { mutateAsync: createTeam, isPending: isCreatingTeam } = useCreateTeam(dataConnect); + const { mutateAsync: createTeam } = useCreateTeam(dataConnect); const handleChange = (field: string, value: any) => { setFormData(prev => ({ ...prev, [field]: value })); diff --git a/apps/web/src/features/business/rates/ServiceRates.tsx b/apps/web/src/features/business/rates/ServiceRates.tsx index 3cbc12df..21f597e9 100644 --- a/apps/web/src/features/business/rates/ServiceRates.tsx +++ b/apps/web/src/features/business/rates/ServiceRates.tsx @@ -28,9 +28,7 @@ import { useCreateCustomRateCard, useUpdateCustomRateCard, useDeleteCustomRateCard, - useCreateVendorRate, useUpdateVendorRate, - useDeleteVendorRate, useGetVendorByUserId } from "@/dataconnect-generated/react"; import RateCardModal from "./components/RateCardModal"; @@ -111,9 +109,7 @@ function VendorCompanyPricebookView({ const { mutate: updateCustomRateCard } = useUpdateCustomRateCard(); const { mutate: deleteCustomRateCard } = useDeleteCustomRateCard(); - const { mutate: createVendorRate } = useCreateVendorRate(); const { mutate: updateVendorRate } = useUpdateVendorRate(); - const { mutate: deleteVendorRate } = useDeleteVendorRate(); const handleUpdateVendorRate = (vars: any) => { updateVendorRate(vars, { @@ -146,8 +142,13 @@ function VendorCompanyPricebookView({ }, [vendorRates, vendorName]); const CATEGORIES = useMemo(() => { - const cats = new Set(vendorRates.map(r => r.category).filter(Boolean)); - return Array.from(cats); + const categories = vendorRates.reduce((acc, r) => { + if (r.category) { + acc.push(String(r.category)); + } + return acc; + }, []); + return Array.from(new Set(categories)); }, [vendorRates]); const handleSaveRateCard = (cardData: any) => { @@ -233,7 +234,6 @@ function VendorCompanyPricebookView({ return rates.map((r: any) => { const parsed = parseRoleName(r.roleName || ""); - const position = parsed.position; // Apply discount if it's a custom rate card const proposedRate = r.clientRate * (1 - discount / 100); @@ -470,6 +470,9 @@ function VendorCompanyPricebookView({
{RATE_CARDS.map((tab) => { const cardData = customRateCards.find((c) => c.name === tab); + if (!cardData) { + return null; + } const isRenaming = renamingCard === tab; if (isRenaming) { diff --git a/apps/web/src/features/finance/invoices/InvoiceDetail.tsx b/apps/web/src/features/finance/invoices/InvoiceDetail.tsx index 6c8c5313..b4a19a40 100644 --- a/apps/web/src/features/finance/invoices/InvoiceDetail.tsx +++ b/apps/web/src/features/finance/invoices/InvoiceDetail.tsx @@ -6,10 +6,8 @@ import { InvoiceStatus } from "@/dataconnect-generated"; import { useGetInvoiceById, useUpdateInvoice, useListRecentPaymentsByInvoiceId } from "@/dataconnect-generated/react"; import { dataConnect } from "@/features/auth/firebase"; import DashboardLayout from "@/features/layouts/DashboardLayout"; -import type { RootState } from "@/store/store"; import { format, parseISO } from "date-fns"; import { ArrowLeft, Download, Mail, CheckCircle, FileText, User, Calendar, MapPin, DollarSign } from "lucide-react"; -import { useSelector } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; const statusConfig: Record = { @@ -25,14 +23,13 @@ const statusConfig: Record = { export default function InvoiceDetail() { const navigate = useNavigate(); const { id: invoiceId } = useParams<{ id: string }>(); - const { user } = useSelector((state: RootState) => state.auth); // Fetch Invoice Data const { data: invoiceData, isLoading: loadingInvoice } = useGetInvoiceById(dataConnect, { id: invoiceId! }); const invoice = invoiceData?.invoice; // Fetch Payment History - const { data: paymentsData, isLoading: loadingPayments } = useListRecentPaymentsByInvoiceId(dataConnect, { invoiceId: invoiceId! }); + const { data: paymentsData } = useListRecentPaymentsByInvoiceId(dataConnect, { invoiceId: invoiceId! }); const payments = paymentsData?.recentPayments || []; // Mutations diff --git a/apps/web/src/features/finance/invoices/InvoiceEditor.tsx b/apps/web/src/features/finance/invoices/InvoiceEditor.tsx index e4d48597..5153cb3d 100644 --- a/apps/web/src/features/finance/invoices/InvoiceEditor.tsx +++ b/apps/web/src/features/finance/invoices/InvoiceEditor.tsx @@ -15,12 +15,9 @@ import { useToast } from "@/common/components/ui/use-toast"; import { InvoiceStatus, InovicePaymentTerms } from "@/dataconnect-generated"; import { useCreateInvoice, - useCreateInvoiceTemplate, - useDeleteInvoiceTemplate, useGetInvoiceById, useListBusinesses, useListInvoices, - useListInvoiceTemplates, useListOrders, useListStaff, useListVendorRates, @@ -71,9 +68,6 @@ export default function InvoiceEditor() { const { data: staffData } = useListStaff(dataConnect); const staffDirectory = staffData?.staffs || []; - const { data: templatesData, refetch: refetchTemplates } = useListInvoiceTemplates(dataConnect); - const templates = templatesData?.invoiceTemplates || []; - const { data: currentInvoiceData } = useGetInvoiceById(dataConnect, { id: effectiveInvoiceId || "" }, { enabled: isEdit && !!effectiveInvoiceId }); const existingInvoice = currentInvoiceData?.invoice; @@ -176,7 +170,7 @@ export default function InvoiceEditor() { phone: existingInvoice.business?.phone || "", email: existingInvoice.business?.email || "", address: existingInvoice.business?.address || "", - manager_name: existingInvoice.business?.contactName || "", + manager_name: existingInvoice.managerName || "", hub_name: existingInvoice.hub || "", vendor_id: existingInvoice.vendorNumber || "" }, @@ -198,21 +192,6 @@ export default function InvoiceEditor() { const selectedBusiness = businesses.find(b => b.id === selectedClientId); if (!selectedBusiness || !position) return 0; - const businessName = selectedBusiness.businessName || ""; - const extractCompanyName = (name: string) => { - if (!name) return ''; - return name.split(/\s*[-–]\s*/)[0].trim(); - }; - const mainCompanyName = extractCompanyName(businessName); - - // Logic similar to MVP - const clientSpecificRate = vendorRates.find(rate => - rate.roleName?.toLowerCase() === position.toLowerCase() && - rate.client_name === businessName // This field might be different in Data Connect, checking listVendorRates output - ); - - if (clientSpecificRate) return clientSpecificRate.clientRate || 0; - const defaultRate = vendorRates.find(rate => rate.roleName?.toLowerCase() === position.toLowerCase() ); @@ -314,157 +293,6 @@ export default function InvoiceEditor() { }); }; - const handleDuplicateInvoice = (invoice: any) => { - const newStaffEntries = (invoice.roles || []).map((entry: any) => ({ - ...entry, - date: format(new Date(), 'MM/dd/yyyy') - })); - - const newInvoiceNumber = generateInvoiceNumber(invoice.business?.businessName || '', invoices); - - setFormData({ - invoice_number: newInvoiceNumber, - event_id: "", - event_name: invoice.order?.eventName || "", - invoice_date: format(new Date(), 'yyyy-MM-dd'), - due_date: format(addDays(new Date(), 45), 'yyyy-MM-dd'), - payment_terms: invoice.paymentTerms || "NET_45", - hub: invoice.hub || "", - manager: invoice.managerName || "", - vendor_id: invoice.vendorNumber || "", - department: invoice.order?.deparment || "", - po_reference: invoice.order?.poReference || "", - from_company: { - name: invoice.vendor?.companyName || formData.from_company.name, - address: invoice.vendor?.address || formData.from_company.address, - phone: invoice.vendor?.phone || formData.from_company.phone, - email: invoice.vendor?.email || formData.from_company.email, - }, - to_company: { - name: invoice.business?.businessName || "", - phone: invoice.business?.phone || "", - email: invoice.business?.email || "", - address: invoice.business?.address || "", - manager_name: invoice.business?.contactName || "", - hub_name: invoice.hub || "", - vendor_id: invoice.vendorNumber || "" - }, - staff_entries: newStaffEntries, - charges: invoice.charges || [], - other_charges: invoice.otherCharges || 0, - notes: invoice.notes || "", - }); - - if (invoice.businessId) { - setSelectedClientId(invoice.businessId); - } - - toast({ - title: "✅ Invoice Duplicated", - description: `Copied from ${invoice.invoiceNumber} - update dates and details as needed`, - }); - }; - - const { mutate: createTemplate } = useCreateInvoiceTemplate(dataConnect); - const { mutate: deleteTemplate } = useDeleteInvoiceTemplate(dataConnect); - - const handleUseTemplate = (template: any) => { - const newStaffEntries = (template.roles || []).map((entry: any) => ({ - ...entry, - name: "", - date: format(new Date(), 'MM/dd/yyyy'), - worked_hours: 0, - regular_hours: 0, - ot_hours: 0, - dt_hours: 0, - regular_value: 0, - ot_value: 0, - dt_value: 0, - total: 0 - })); - - const newInvoiceNumber = generateInvoiceNumber(template.business?.businessName || '', invoices); - - setFormData((prev: any) => ({ - ...prev, - invoice_number: newInvoiceNumber, - invoice_date: format(new Date(), 'yyyy-MM-dd'), - due_date: format(addDays(new Date(), 45), 'yyyy-MM-dd'), - payment_terms: template.paymentTerms || "NET_45", - hub: template.hub || "", - department: "", - po_reference: template.order?.poReference || "", - from_company: { - name: template.vendor?.companyName || prev.from_company.name, - address: template.vendor?.address || prev.from_company.address, - phone: template.vendor?.phone || prev.from_company.phone, - email: template.vendor?.email || prev.from_company.email, - }, - to_company: { - name: template.business?.businessName || "", - phone: template.business?.phone || "", - email: template.business?.email || "", - address: template.business?.address || "", - manager_name: template.business?.contactName || "", - hub_name: template.hub || "", - vendor_id: template.vendorNumber || "" - }, - staff_entries: newStaffEntries, - charges: template.charges || [], - notes: template.notes || "", - })); - - if (template.businessId) { - setSelectedClientId(template.businessId); - } - - toast({ - title: "✅ Template Applied", - description: `Applied "${template.name}" - fill in staff names and times`, - }); - }; - - const handleSaveTemplate = async (templateName: string) => { - const selectedBusiness = businesses.find(b => b.id === selectedClientId); - - await createTemplate({ - name: templateName, - ownerId: "00000000-0000-0000-0000-000000000000", // placeholder, usually from auth - businessId: selectedClientId || undefined, - vendorId: "00000000-0000-0000-0000-000000000000", // placeholder - paymentTerms: formData.payment_terms as InovicePaymentTerms, - invoiceNumber: formData.invoice_number, - issueDate: new Date(formData.invoice_date).toISOString(), - dueDate: new Date(formData.due_date).toISOString(), - hub: formData.hub, - managerName: formData.manager, - roles: formData.staff_entries, - charges: formData.charges, - otherCharges: parseFloat(formData.other_charges) || 0, - subtotal: totals.subtotal, - amount: totals.grandTotal, - notes: formData.notes, - staffCount: formData.staff_entries.length, - chargesCount: formData.charges.length, - }); - - refetchTemplates(); - - toast({ - title: "✅ Template Saved", - description: `"${templateName}" can now be reused for future invoices`, - }); - }; - - const handleDeleteTemplate = async (templateId: string) => { - await deleteTemplate({ id: templateId }); - refetchTemplates(); - toast({ - title: "Template Deleted", - description: "Template has been removed", - }); - }; - const parseTimeToMinutes = (timeStr: string) => { if (!timeStr || timeStr === "hh:mm") return null; const match = timeStr.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i); @@ -747,11 +575,11 @@ export default function InvoiceEditor() {

Disputed Items Highlighted

{disputedIndices.length} line item(s) have been disputed by the client. - Reason: {existingInvoice.dispute_reason || "Not specified"} + Reason: {existingInvoice.disputeReason || "Not specified"}

- {existingInvoice.dispute_details && ( + {existingInvoice.disputeDetails && (

- "{existingInvoice.dispute_details}" + "{existingInvoice.disputeDetails}"

)}

@@ -775,7 +603,7 @@ export default function InvoiceEditor() {

Quickly fill invoice from a completed event's shifts

- { const event = events.find(e => e.id === val); if (event) handleImportFromEvent(event); }}> @@ -1315,4 +1143,4 @@ export default function InvoiceEditor() { ); -} \ No newline at end of file +} diff --git a/apps/web/src/features/finance/invoices/InvoiceList.tsx b/apps/web/src/features/finance/invoices/InvoiceList.tsx index 723ae2fd..b9df1a67 100644 --- a/apps/web/src/features/finance/invoices/InvoiceList.tsx +++ b/apps/web/src/features/finance/invoices/InvoiceList.tsx @@ -59,7 +59,6 @@ export default function InvoiceList() { // If user is client, they should see their invoices. If admin, they see all. const userRole = user?.userRole?.toUpperCase(); const isClient = userRole === "CLIENT"; - const isVendor = userRole === "VENDOR"; if (isClient && inv.businessId !== user?.uid) return false; // In a real scenario, we'd match vendorId for vendor users diff --git a/apps/web/src/features/operations/orders/EditOrder.tsx b/apps/web/src/features/operations/orders/EditOrder.tsx index 83d62fb2..cb4ff7bb 100644 --- a/apps/web/src/features/operations/orders/EditOrder.tsx +++ b/apps/web/src/features/operations/orders/EditOrder.tsx @@ -10,9 +10,48 @@ import OrderReductionAlert from "./components/OrderReductionAlert"; import EventFormWizard from "./components/EventFormWizard"; import { useToast } from "@/common/components/ui/use-toast"; import { useGetOrderById, useUpdateOrder, useListStaff } from "@/dataconnect-generated/react"; +import type { UpdateOrderVariables } from "@/dataconnect-generated"; import { dataConnect } from "@/features/auth/firebase"; import type { RootState } from "@/store/store"; +const asRecord = (value: unknown): Record | null => { + if (typeof value === "object" && value !== null) { + return value as Record; + } + return null; +}; + +const getTeamHubId = (value: unknown): string | null => { + const record = asRecord(value); + if (!record) return null; + + const directValue = record.teamHubId; + if (typeof directValue === "string" && directValue.length > 0) { + return directValue; + } + + const teamHub = asRecord(record.teamHub); + const nestedValue = teamHub?.id; + if (typeof nestedValue === "string" && nestedValue.length > 0) { + return nestedValue; + } + + return null; +}; + +const getStringField = (value: unknown, keys: string[]): string | null => { + const record = asRecord(value); + if (!record) return null; + + for (const key of keys) { + const fieldValue = record[key]; + if (typeof fieldValue === "string" && fieldValue.length > 0) { + return fieldValue; + } + } + + return null; +}; export default function EditOrder() { const navigate = useNavigate(); @@ -22,7 +61,7 @@ export default function EditOrder() { const { user } = useSelector((state: RootState) => state.auth); const [showReductionAlert, setShowReductionAlert] = useState(false); - const [pendingUpdate, setPendingUpdate] = useState(null); + const [pendingUpdate, setPendingUpdate] = useState(null); const [originalRequested, setOriginalRequested] = useState(0); const { data: orderData, isLoading: isOrderLoading } = useGetOrderById( @@ -86,11 +125,37 @@ export default function EditOrder() { const assignedStaff = Array.isArray(event?.assignedStaff) ? event!.assignedStaff : []; const assignedCount = assignedStaff.length; - const isVendor = user?.userRole === 'vendor' || (user as any)?.role === 'vendor'; + const isVendor = user?.userRole?.toLowerCase() === 'vendor'; + + if (!eventId) return; + + const teamHubId = getTeamHubId(eventData) ?? getTeamHubId(event); + if (!teamHubId) { + toast({ + title: "Missing Team Hub", + description: "Cannot update order because no team hub is associated.", + variant: "destructive", + }); + return; + } + + const updatePayload: UpdateOrderVariables = { + id: eventId, + teamHubId, + eventName: eventData.event_name, + date: eventData.date, + startDate: eventData.startDate || eventData.date, + endDate: eventData.endDate, + notes: eventData.notes, + shifts: eventData.shifts, + requested: totalRequested, + total: eventData.total, + poReference: eventData.po_reference, + }; // If client is reducing headcount and vendor has already assigned staff if (!isVendor && totalRequested < originalRequested && assignedCount > totalRequested) { - setPendingUpdate({ ...eventData, requested: totalRequested }); + setPendingUpdate(updatePayload); setShowReductionAlert(true); toast({ @@ -100,52 +165,46 @@ export default function EditOrder() { return; } - if (eventId) { - // Normal update - updateOrderMutation.mutate({ - id: eventId, - eventName: eventData.event_name, - date: eventData.date, - startDate: eventData.startDate || eventData.date, - endDate: eventData.endDate, - notes: eventData.notes, - shifts: eventData.shifts, - requested: totalRequested, - total: eventData.total, - poReference: eventData.po_reference, - }); - } + updateOrderMutation.mutate(updatePayload); }; const handleAutoUnassign = async () => { if (!pendingUpdate || !event || !eventId) return; - const assignedStaff = Array.isArray(event.assignedStaff) ? (event.assignedStaff as any[]) : []; - const excessCount = assignedStaff.length - pendingUpdate.requested; + const assignedStaff = Array.isArray(event.assignedStaff) ? event.assignedStaff : []; + const requestedCount = pendingUpdate.requested ?? 0; + const excessCount = Math.max(0, assignedStaff.length - requestedCount); // Calculate reliability scores for assigned staff - const staffWithScores = assignedStaff.map(assigned => { - const staffInfo = allStaff.find(s => s.id === assigned.staff_id || s.id === assigned.staffId); + const staffWithScores = assignedStaff.map((assigned) => { + const staffId = getStringField(assigned, ["staff_id", "staffId"]); + const staffName = getStringField(assigned, ["staffName", "staff_name"]); + const role = getStringField(assigned, ["role"]) ?? ""; + const staffInfo = staffId ? allStaff.find((s) => s.id === staffId) : undefined; return { - ...assigned, + staffId, + staffName, + role, reliability: staffInfo?.averageRating ? staffInfo.averageRating * 20 : 50, // Convert 0-5 to 0-100 }; }); // Sort by reliability (lowest first) - staffWithScores.sort((a: any, b: any) => a.reliability - b.reliability); + staffWithScores.sort((a, b) => a.reliability - b.reliability); // Remove lowest reliability staff const staffToKeep = staffWithScores.slice(excessCount); + const assignedStaffPayload = staffToKeep + .filter((s) => typeof s.staffId === "string" && s.staffId.length > 0) + .map((s) => ({ + staffId: s.staffId as string, + staffName: s.staffName ?? "Unknown", + role: s.role, + })); await updateOrderMutation.mutateAsync({ - id: eventId, ...pendingUpdate, - assignedStaff: staffToKeep.map((s: any) => ({ - staffId: s.staffId || s.staff_id, - staffName: s.staffName || s.staff_name, - role: s.role - })) + assignedStaff: assignedStaffPayload, }); setShowReductionAlert(false); @@ -203,14 +262,16 @@ export default function EditOrder() {
{ - const staffInfo = allStaff.find(s => s.id === assigned.staffId || s.id === assigned.staff_id); + lowReliabilityStaff={(Array.isArray(event.assignedStaff) ? event.assignedStaff : []).map((assigned) => { + const staffId = getStringField(assigned, ["staffId", "staff_id"]); + const staffName = getStringField(assigned, ["staffName", "staff_name"]) ?? "Unknown"; + const staffInfo = staffId ? allStaff.find((s) => s.id === staffId) : undefined; return { - name: assigned.staffName || assigned.staff_name, + name: staffName, reliability: staffInfo?.averageRating ? staffInfo.averageRating * 20 : 50 }; }).sort((a, b) => a.reliability - b.reliability)} @@ -219,10 +280,10 @@ export default function EditOrder() { )} navigate(createPageUrl("Events"))} />
diff --git a/apps/web/src/features/operations/orders/OrderDetail.tsx b/apps/web/src/features/operations/orders/OrderDetail.tsx index 2cb4918f..b5251c74 100644 --- a/apps/web/src/features/operations/orders/OrderDetail.tsx +++ b/apps/web/src/features/operations/orders/OrderDetail.tsx @@ -9,7 +9,7 @@ import { Button } from "@/common/components/ui/button"; import { Badge } from "@/common/components/ui/badge"; import DashboardLayout from "@/features/layouts/DashboardLayout"; import { useGetOrderById, useUpdateOrder, useListShiftRolesByBusinessAndOrder } from "@/dataconnect-generated/react"; -import { OrderStatus } from "@/dataconnect-generated"; +import { OrderStatus, type UpdateOrderVariables } from "@/dataconnect-generated"; import { dataConnect } from "@/features/auth/firebase"; import { useToast } from "@/common/components/ui/use-toast"; import AssignStaffModal from "./components/AssignStaffModal"; @@ -37,6 +37,31 @@ const safeFormatDateTime = (value?: string | null): string => { } }; +const resolveTeamHubId = (value: unknown): string | null => { + if (typeof value !== "object" || value === null) { + return null; + } + + const record = value as Record; + const directValue = record.teamHubId; + if (typeof directValue === "string" && directValue.length > 0) { + return directValue; + } + + const teamHub = record.teamHub; + if (typeof teamHub !== "object" || teamHub === null) { + return null; + } + + const teamHubRecord = teamHub as Record; + const nestedValue = teamHubRecord.id; + if (typeof nestedValue === "string" && nestedValue.length > 0) { + return nestedValue; + } + + return null; +}; + const getStatusBadge = (status: OrderStatus) => { switch (status) { case OrderStatus.FULLY_STAFFED: @@ -105,7 +130,6 @@ export default function OrderDetail() { // Fetch real shift roles to get IDs and accurate counts const { data: shiftRolesData, - isLoading: isLoadingShifts, refetch: refetchShifts } = useListShiftRolesByBusinessAndOrder( dataConnect, @@ -142,10 +166,22 @@ export default function OrderDetail() { const handleCancel = () => { if (!order || !id || !canModify) return; - cancelMutation.mutate({ + const teamHubId = resolveTeamHubId(order); + if (!teamHubId) { + toast({ + title: "Missing Team Hub", + description: "Cannot cancel this order because team hub information is missing.", + variant: "destructive", + }); + return; + } + + const payload: UpdateOrderVariables = { id, + teamHubId, status: OrderStatus.CANCELLED, - }); + }; + cancelMutation.mutate(payload); }; const handleEdit = () => { @@ -163,7 +199,7 @@ export default function OrderDetail() { if (shiftRolesData?.shiftRoles && shiftRolesData.shiftRoles.length > 0) { return shiftRolesData.shiftRoles; } - return Array.isArray(order?.shifts) ? (order!.shifts as any[]) : []; + return Array.isArray(order?.shifts) ? order.shifts : []; }, [shiftRolesData, order?.shifts]); const totalRequested = order?.requested ?? 0; diff --git a/apps/web/src/features/operations/orders/OrderList.tsx b/apps/web/src/features/operations/orders/OrderList.tsx index 14463846..6cc6e348 100644 --- a/apps/web/src/features/operations/orders/OrderList.tsx +++ b/apps/web/src/features/operations/orders/OrderList.tsx @@ -13,21 +13,19 @@ import DashboardLayout from "@/features/layouts/DashboardLayout"; import { Search, Calendar, - Filter, ArrowRight, Clock, CheckCircle, AlertTriangle, - XCircle, FileText } from "lucide-react"; -import React, { useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useSelector } from "react-redux"; import type { RootState } from "@/store/store"; import { useListOrders, useListBusinesses } from "@/dataconnect-generated/react"; import { dataConnect } from "@/features/auth/firebase"; -import { format, isWithinInterval, parseISO, startOfDay, endOfDay } from "date-fns"; +import { format, isWithinInterval, startOfDay, endOfDay } from "date-fns"; import { OrderStatus } from "@/dataconnect-generated"; export default function OrderList() { diff --git a/apps/web/src/features/operations/orders/components/AssignStaffModal.tsx b/apps/web/src/features/operations/orders/components/AssignStaffModal.tsx index f622884d..4c048be6 100644 --- a/apps/web/src/features/operations/orders/components/AssignStaffModal.tsx +++ b/apps/web/src/features/operations/orders/components/AssignStaffModal.tsx @@ -1,6 +1,5 @@ -import React, { useState, useMemo } from "react"; -import { Search, Check, X, UserPlus, Star, Clock, AlertTriangle } from "lucide-react"; -import { format, isSameDay, parseISO } from "date-fns"; +import { useState, useMemo } from "react"; +import { Search, Check, Star, Clock, AlertTriangle } from "lucide-react"; import { Dialog, @@ -20,7 +19,6 @@ import { useCreateAssignment, useUpdateShiftRole, useListAssignments, - useListStaffAvailabilitiesByDay } from "@/dataconnect-generated/react"; import { dataConnect } from "@/features/auth/firebase"; import { AssignmentStatus } from "@/dataconnect-generated"; @@ -38,7 +36,6 @@ export default function AssignStaffModal({ isOpen, onClose, shift, onSuccess }: const [selectedStaff, setSelectedStaff] = useState(null); const vendorId = shift.shift?.order?.vendorId || shift.order?.vendorId; - const shiftDate = shift.shift?.date || shift.date; const shiftStartTime = shift.startTime || shift.start; const shiftEndTime = shift.endTime || shift.end; diff --git a/apps/web/src/features/operations/schedule/Schedule.tsx b/apps/web/src/features/operations/schedule/Schedule.tsx index 0deb571b..dffd5c20 100644 --- a/apps/web/src/features/operations/schedule/Schedule.tsx +++ b/apps/web/src/features/operations/schedule/Schedule.tsx @@ -46,7 +46,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/common/components/ui/tabs"; import DashboardLayout from "@/features/layouts/DashboardLayout"; import { useListOrders, useUpdateOrder } from "@/dataconnect-generated/react"; import { dataConnect } from "@/features/auth/firebase"; -import { OrderStatus } from "@/dataconnect-generated"; +import { OrderStatus, type UpdateOrderVariables } from "@/dataconnect-generated"; import { useToast } from "@/common/components/ui/use-toast"; /** @@ -83,6 +83,35 @@ const safeParseDate = (dateString: any) => { } }; +const isViewMode = (value: string): value is "day" | "week" | "month" => { + return value === "day" || value === "week" || value === "month"; +}; + +const resolveTeamHubId = (value: unknown): string | null => { + if (typeof value !== "object" || value === null) { + return null; + } + + const record = value as Record; + const directValue = record.teamHubId; + if (typeof directValue === "string" && directValue.length > 0) { + return directValue; + } + + const teamHub = record.teamHub; + if (typeof teamHub !== "object" || teamHub === null) { + return null; + } + + const teamHubRecord = teamHub as Record; + const nestedValue = teamHubRecord.id; + if (typeof nestedValue === "string" && nestedValue.length > 0) { + return nestedValue; + } + + return null; +}; + export default function Schedule() { const navigate = useNavigate(); const { toast } = useToast(); @@ -204,10 +233,22 @@ export default function Schedule() { const confirmReschedule = () => { if (rescheduleData) { - updateOrderMutation.mutate({ + const teamHubId = resolveTeamHubId(rescheduleData.order); + if (!teamHubId) { + toast({ + title: "Missing Team Hub", + description: "Cannot reschedule this order because team hub information is missing.", + variant: "destructive", + }); + return; + } + + const payload: UpdateOrderVariables = { id: rescheduleData.order.id, - date: rescheduleData.newDate.toISOString() - }); + teamHubId, + date: rescheduleData.newDate.toISOString(), + }; + updateOrderMutation.mutate(payload); } }; @@ -287,7 +328,15 @@ export default function Schedule() { - setViewMode(v as any)} className="w-full md:w-auto"> + { + if (isViewMode(value)) { + setViewMode(value); + } + }} + className="w-full md:w-auto" + > Day Week diff --git a/apps/web/src/features/operations/tasks/TaskBoard.tsx b/apps/web/src/features/operations/tasks/TaskBoard.tsx index f95e146a..1de29ddf 100644 --- a/apps/web/src/features/operations/tasks/TaskBoard.tsx +++ b/apps/web/src/features/operations/tasks/TaskBoard.tsx @@ -39,7 +39,7 @@ export default function TaskBoard() { const [conditionalColoring, setConditionalColoring] = useState(true); // Queries - const { data: shiftsData, isLoading: shiftsLoading } = useListShifts(dataConnect); + const { data: shiftsData } = useListShifts(dataConnect); const { data: clientsData } = useListBusinesses(dataConnect); const shifts = useMemo(() => shiftsData?.shifts || [], [shiftsData]); diff --git a/apps/web/src/features/operations/tasks/TaskCard.tsx b/apps/web/src/features/operations/tasks/TaskCard.tsx index 47bb7c3f..786500b0 100644 --- a/apps/web/src/features/operations/tasks/TaskCard.tsx +++ b/apps/web/src/features/operations/tasks/TaskCard.tsx @@ -31,7 +31,7 @@ export default function TaskCard({ provided, onClick, itemHeight = "normal", - conditionalColoring = true + conditionalColoring: _conditionalColoring = true }: TaskCardProps) { const heightClasses = { compact: "p-3", diff --git a/apps/web/src/features/workforce/directory/AddStaff.tsx b/apps/web/src/features/workforce/directory/AddStaff.tsx index c108389f..343f5c1d 100644 --- a/apps/web/src/features/workforce/directory/AddStaff.tsx +++ b/apps/web/src/features/workforce/directory/AddStaff.tsx @@ -164,7 +164,8 @@ export default function AddStaff() { { + onChange={(e) => { + const checked = e.target.checked; const updatedSkills = checked ? [...(field.value || []), skill] : field.value?.filter((s: string) => s !== skill); diff --git a/apps/web/src/features/workforce/directory/StaffList.tsx b/apps/web/src/features/workforce/directory/StaffList.tsx index 32b4d788..a3efe9d3 100644 --- a/apps/web/src/features/workforce/directory/StaffList.tsx +++ b/apps/web/src/features/workforce/directory/StaffList.tsx @@ -18,7 +18,7 @@ const ITEMS_PER_PAGE = 10; function StaffActiveStatus({ staffId }: { staffId: string }) { const { data: staffDetail, isLoading } = useGetStaffById(dataConnect, { id: staffId }); - const getLastActiveText = (lastActive?: string) => { + const getLastActiveText = (lastActive?: string | null) => { if (!lastActive) return 'Never'; try { const date = new Date(lastActive); diff --git a/apps/web/src/features/workforce/directory/components/EmployeeCard.tsx b/apps/web/src/features/workforce/directory/components/EmployeeCard.tsx index 020ac0f0..ccf313c7 100644 --- a/apps/web/src/features/workforce/directory/components/EmployeeCard.tsx +++ b/apps/web/src/features/workforce/directory/components/EmployeeCard.tsx @@ -74,7 +74,7 @@ export default function EmployeeCard({ staff }: EmployeeCardProps) { const coveragePercentage = staff.shift_coverage_percentage || 0; const cancellationCount = staff.cancellation_count || 0; const noShowCount = staff.no_show_count || 0; - const rating = staff.rating || 0; + const rating = staff.averageRating || 0; const reliabilityScore = staff.reliability_score || 0; const reliability = getReliabilityColor(reliabilityScore); diff --git a/apps/web/src/features/workforce/documents/DocumentVault.tsx b/apps/web/src/features/workforce/documents/DocumentVault.tsx index fc147dbf..02404377 100644 --- a/apps/web/src/features/workforce/documents/DocumentVault.tsx +++ b/apps/web/src/features/workforce/documents/DocumentVault.tsx @@ -107,7 +107,7 @@ export default function DocumentVault() { const filteredStaff = useMemo(() => { let result = [...staff]; if (isVendor && user?.uid) { - result = result.filter(s => s.ownerId === user.uid || s.createdBy === user.email); + result = result.filter(s => s.ownerId === user.uid); } if (searchTerm) { result = result.filter(s => @@ -472,7 +472,7 @@ export default function DocumentVault() { Employees - {availableDocTypes.map((type, idx) => ( + {availableDocTypes.map((type) => (
{type.name} diff --git a/apps/web/src/services/firestoreService.ts b/apps/web/src/services/firestoreService.ts index 6b4c8199..cf168b00 100644 --- a/apps/web/src/services/firestoreService.ts +++ b/apps/web/src/services/firestoreService.ts @@ -1,5 +1,6 @@ import { getFirestore, doc, getDoc } from "firebase/firestore"; import { app } from "../features/auth/firebase"; +import { getDashboardPath as getDashboardPathFromRole } from "../features/auth/roleUtils"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - generated dataconnect types may not be resolvable in this context import { getUserById } from "@/dataconnect-generated"; @@ -68,11 +69,5 @@ export const fetchUserData = async (uid: string): Promise => { * @returns The appropriate dashboard path */ export const getDashboardPath = (userRole: string): string => { - const roleMap: Record = { - admin: "/dashboard/admin", - client: "/dashboard/client", - vendor: "/dashboard/vendor", - }; - - return roleMap[userRole.toLowerCase()] || "/dashboard/client"; + return getDashboardPathFromRole(userRole); }; diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts new file mode 100644 index 00000000..f6755d58 --- /dev/null +++ b/apps/web/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + coverage: { + enabled: false, + }, + }, +}) diff --git a/codemagic.yaml b/codemagic.yaml index caee57e3..1c6c846a 100644 --- a/codemagic.yaml +++ b/codemagic.yaml @@ -68,7 +68,48 @@ distribute-ios-script: &distribute-ios-script --groups "$FIREBASE_TESTER_GROUPS" \ --token $FIREBASE_TOKEN +# Reusable script for web quality checks +web-quality-script: &web-quality-script + name: ✅ Web Quality Checks + script: | + npm install -g pnpm + cd apps/web + pnpm install --frozen-lockfile + pnpm lint + pnpm test + pnpm build + +# Reusable script for mobile quality checks +mobile-quality-script: &mobile-quality-script + name: ✅ Mobile Quality Checks + script: | + dart pub global activate melos + export PATH="$PATH":"$HOME/.pub-cache/bin" + make mobile-install + make mobile-analyze + make mobile-test + workflows: + # ================================================================================= + # Quality workflow (Web + Mobile) + # ================================================================================= + quality-gates-dev: + name: ✅ Quality Gates (Dev) + working_directory: . + instance_type: mac_mini_m2 + max_build_duration: 60 + environment: + flutter: stable + xcode: latest + cocoapods: default + groups: + - client_app_dev_credentials + vars: + ENV: dev + scripts: + - *web-quality-script + - *mobile-quality-script + # ================================================================================= # Base workflow for client_app # ================================================================================= @@ -287,4 +328,4 @@ workflows: scripts: - *staff-app-ios-build-script - *distribute-ios-script - \ No newline at end of file + diff --git a/makefiles/web.mk b/makefiles/web.mk index 4e4ba6b7..997e54fe 100644 --- a/makefiles/web.mk +++ b/makefiles/web.mk @@ -1,6 +1,6 @@ # --- Web App Development --- -.PHONY: web-install web-info web-dev web-build web-lint web-preview web-deploy +.PHONY: web-install web-info web-dev web-build web-lint web-test web-preview web-deploy WEB_DIR := apps/web @@ -30,6 +30,10 @@ web-lint: @echo "--> Linting web frontend..." @cd $(WEB_DIR) && pnpm lint +web-test: + @echo "--> Running web frontend tests..." + @cd $(WEB_DIR) && pnpm test + web-preview: @echo "--> Previewing web frontend build..." @cd $(WEB_DIR) && pnpm preview