diff --git a/fdev-ffrontend/.env.development b/fdev-ffrontend/.env.development deleted file mode 100644 index b44b7f8f..00000000 --- a/fdev-ffrontend/.env.development +++ /dev/null @@ -1,23 +0,0 @@ -# API Configuration -VITE_API_URL=http://localhost:8000/api/v1 -VITE_API_TIMEOUT=30000 -VITE_API_RETRIES=3 -VITE_API_RETRY_DELAY=1000 -VITE_API_LOGGING=true -VITE_API_CACHING=true -VITE_API_CACHE_TIMEOUT=300000 - -# Feature Flags -VITE_ENABLE_WEBSOCKETS=false -VITE_ENABLE_OFFLINE=false -VITE_ENABLE_OPTIMISTIC_UPDATES=true -VITE_ENABLE_DEDUPLICATION=true -VITE_ENABLE_METRICS=false - -# Stripe Configuration (Spanish Market) -VITE_STRIPE_PUBLISHABLE_KEY=pk_test_example_key_for_development -VITE_STRIPE_WEBHOOK_SECRET=whsec_example_webhook_secret_for_development - -# Development Flags -VITE_BYPASS_PAYMENT=true -VITE_DEV_MODE=true \ No newline at end of file diff --git a/fdev-ffrontend/.env.production b/fdev-ffrontend/.env.production deleted file mode 100644 index f6bda9e6..00000000 --- a/fdev-ffrontend/.env.production +++ /dev/null @@ -1,23 +0,0 @@ -# API Configuration -VITE_API_URL=https://api.pania.es/api/v1 -VITE_API_TIMEOUT=30000 -VITE_API_RETRIES=3 -VITE_API_RETRY_DELAY=1000 -VITE_API_LOGGING=false -VITE_API_CACHING=true -VITE_API_CACHE_TIMEOUT=300000 - -# Feature Flags -VITE_ENABLE_WEBSOCKETS=true -VITE_ENABLE_OFFLINE=true -VITE_ENABLE_OPTIMISTIC_UPDATES=true -VITE_ENABLE_DEDUPLICATION=true -VITE_ENABLE_METRICS=true - -# Stripe Configuration (Spanish Market) -VITE_STRIPE_PUBLISHABLE_KEY=pk_live_your_production_stripe_key -VITE_STRIPE_WEBHOOK_SECRET=whsec_your_production_webhook_secret - -# Development Flags (DISABLED IN PRODUCTION) -VITE_BYPASS_PAYMENT=false -VITE_DEV_MODE=false \ No newline at end of file diff --git a/fdev-ffrontend/Dockerfile.development b/fdev-ffrontend/Dockerfile.development deleted file mode 100644 index 6069bda8..00000000 --- a/fdev-ffrontend/Dockerfile.development +++ /dev/null @@ -1,38 +0,0 @@ -# frontend/Dockerfile.development - FIXED VERSION -FROM node:18-alpine - -# Install curl for healthchecks -RUN apk add --no-cache curl - -# Set working directory -WORKDIR /app - -# Create non-root user for security but don't switch yet -RUN addgroup -g 1001 -S nodejs && \ - adduser -S reactjs -u 1001 -G nodejs - -# Copy package files first (better caching) -COPY package*.json ./ - -# Install all dependencies (including dev dependencies) as root -RUN npm ci --verbose && \ - npm cache clean --force - -# Copy source code -COPY . . - -# Change ownership of all files to the non-root user -RUN chown -R reactjs:nodejs /app - -# Now switch to non-root user -USER reactjs - -# Expose port 3000 (Vite default) -EXPOSE 3000 - -# Add healthcheck -HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ - CMD curl -f http://localhost:3000/ || exit 1 - -# Start development server with host binding -CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/fdev-ffrontend/Dockerfile.production b/fdev-ffrontend/Dockerfile.production deleted file mode 100644 index 98ba917c..00000000 --- a/fdev-ffrontend/Dockerfile.production +++ /dev/null @@ -1,41 +0,0 @@ -# frontend/Dockerfile.production -# Multi-stage build for production - -# Build stage -FROM node:18-alpine as builder - -WORKDIR /app - -# Copy package files -COPY package*.json ./ - -# Install dependencies -RUN npm ci --only=production - -# Copy source code -COPY . . - -# Build the application -RUN npm run build - -# Production stage -FROM nginx:alpine - -# Install curl for healthchecks -RUN apk add --no-cache curl - -# Copy built app from builder stage -COPY --from=builder /app/dist /usr/share/nginx/html - -# Copy nginx configuration -COPY nginx.conf /etc/nginx/nginx.conf - -# Expose port 80 -EXPOSE 80 - -# Add healthcheck -HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ - CMD curl -f http://localhost/ || exit 1 - -# Start nginx -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/fdev-ffrontend/index.html b/fdev-ffrontend/index.html deleted file mode 100644 index 66ef8f1b..00000000 --- a/fdev-ffrontend/index.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - PanIA - Inteligencia Artificial para tu Panadería en Madrid - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
🥖
-
Cargando PanIA...
-
-
- - -
- - - - - - - - \ No newline at end of file diff --git a/fdev-ffrontend/nginx.conf b/fdev-ffrontend/nginx.conf deleted file mode 100644 index 149fec06..00000000 --- a/fdev-ffrontend/nginx.conf +++ /dev/null @@ -1,70 +0,0 @@ -# frontend/nginx.conf -events { - worker_connections 1024; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - # Enable gzip compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_proxied any; - gzip_comp_level 6; - gzip_types - text/plain - text/css - text/xml - text/javascript - application/json - application/javascript - application/xml+rss - application/atom+xml - image/svg+xml; - - server { - listen 80; - server_name localhost; - root /usr/share/nginx/html; - index index.html; - - # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - - # Handle React Router - location / { - try_files $uri $uri/ /index.html; - } - - # API proxy to backend - location /api/ { - proxy_pass http://bakery-gateway:8000/api/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - 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; - proxy_cache_bypass $http_upgrade; - } - - # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # Health check endpoint - location /health { - access_log off; - return 200 "healthy\n"; - add_header Content-Type text/plain; - } - } -} \ No newline at end of file diff --git a/fdev-ffrontend/package-lock.json b/fdev-ffrontend/package-lock.json deleted file mode 100644 index 52888e6f..00000000 --- a/fdev-ffrontend/package-lock.json +++ /dev/null @@ -1,7652 +0,0 @@ -{ - "name": "pania-frontend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "pania-frontend", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@hookform/resolvers": "^3.3.1", - "@reduxjs/toolkit": "^1.9.5", - "@stripe/react-stripe-js": "^3.9.0", - "@stripe/stripe-js": "^7.8.0", - "clsx": "^2.0.0", - "date-fns": "^2.30.0", - "date-fns-tz": "^2.0.0", - "i18next": "^23.4.4", - "i18next-browser-languagedetector": "^7.1.0", - "lucide-react": "^0.263.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-hook-form": "^7.45.4", - "react-hot-toast": "^2.4.1", - "react-i18next": "^13.1.2", - "react-redux": "^8.1.2", - "react-router-dom": "^6.15.0", - "recharts": "^2.8.0", - "tailwind-merge": "^1.14.0", - "zod": "^3.22.2" - }, - "devDependencies": { - "@tailwindcss/forms": "^0.5.4", - "@tailwindcss/typography": "^0.5.9", - "@testing-library/jest-dom": "^5.17.0", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "@types/react": "^18.2.15", - "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "@vitejs/plugin-react": "^4.0.3", - "@vitest/ui": "^0.34.1", - "autoprefixer": "^10.4.14", - "eslint": "^8.45.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", - "postcss": "^8.4.27", - "tailwindcss": "^3.3.0", - "typescript": "^5.0.2", - "vite": "^4.4.5", - "vitest": "^0.34.1" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", - "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": 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.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@hookform/resolvers": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", - "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", - "license": "MIT", - "peerDependencies": { - "react-hook-form": "^7.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@jest/diff-sequences": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz", - "integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/get-type": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", - "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/types": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", - "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/types/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/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@reduxjs/toolkit": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz", - "integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==", - "license": "MIT", - "dependencies": { - "immer": "^9.0.21", - "redux": "^4.2.1", - "redux-thunk": "^2.4.2", - "reselect": "^4.1.8" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18", - "react-redux": "^7.2.1 || ^8.0.2" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.38", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", - "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@stripe/react-stripe-js": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.9.0.tgz", - "integrity": "sha512-pN1Re7zUc3m61FFQROok685g3zsBQRzCmZDmTzO8iPU6zhLvu2JnC0LrG0FCzSp6kgGa8AQSzq4rpFSgyhkjKg==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "@stripe/stripe-js": ">=1.44.1 <8.0.0", - "react": ">=16.8.0 <20.0.0", - "react-dom": ">=16.8.0 <20.0.0" - } - }, - "node_modules/@stripe/stripe-js": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.8.0.tgz", - "integrity": "sha512-DNXRfYUgkZlrniQORbA/wH8CdFRhiBSE0R56gYU0V5vvpJ9WZwvGrz9tBAZmfq2aTgw6SK7mNpmTizGzLWVezw==", - "license": "MIT", - "engines": { - "node": ">=12.16" - } - }, - "node_modules/@tailwindcss/forms": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", - "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mini-svg-data-uri": "^1.2.3" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" - } - }, - "node_modules/@tailwindcss/typography": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", - "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.castarray": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.merge": "^4.6.2", - "postcss-selector-parser": "6.0.10" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", - "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.0.1", - "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", - "aria-query": "^5.0.0", - "chalk": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", - "lodash": "^4.17.15", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=8", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/react": { - "version": "13.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", - "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.5.0", - "@types/react-dom": "^18.0.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@testing-library/react/node_modules/@testing-library/dom": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", - "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@testing-library/react/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/@testing-library/react/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/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/chai": { - "version": "4.3.20", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", - "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai-subset": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.6.tgz", - "integrity": "sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/chai": "<5.2.0" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", - "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", - "license": "MIT", - "dependencies": { - "hoist-non-react-statics": "^3.3.0" - }, - "peerDependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "30.0.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^30.0.0", - "pretty-format": "^30.0.0" - } - }, - "node_modules/@types/jest/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@types/jest/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@types/jest/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", - "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.10.0" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.23", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", - "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/testing-library__jest-dom": { - "version": "5.14.9", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", - "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/jest": "*" - } - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@vitest/expect": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", - "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/expect/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/expect/node_modules/@vitest/utils": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", - "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/expect/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==", - "dev": true, - "license": "MIT", - "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/@vitest/expect/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/runner": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", - "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "0.34.6", - "p-limit": "^4.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/runner/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/runner/node_modules/@vitest/utils": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", - "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/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==", - "dev": true, - "license": "MIT", - "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/@vitest/runner/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", - "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/snapshot": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", - "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "pretty-format": "^29.5.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/snapshot/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/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==", - "dev": true, - "license": "MIT", - "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/@vitest/snapshot/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/spy": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", - "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^2.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/ui": { - "version": "0.34.7", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.34.7.tgz", - "integrity": "sha512-iizUu9R5Rsvsq8FtdJ0suMqEfIsIIzziqnasMHe4VH8vG+FnZSA3UAtCHx6rLeRupIFVAVg7bptMmuvMcsn8WQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "0.34.7", - "fast-glob": "^3.3.0", - "fflate": "^0.8.0", - "flatted": "^3.2.7", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "sirv": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": ">=0.30.1 <1" - } - }, - "node_modules/@vitest/utils": { - "version": "0.34.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.7.tgz", - "integrity": "sha512-ziAavQLpCYS9sLOorGrFFKmy2gnfiNU0ZJ15TsMz/K92NAPS/rp9K4z6AJQQk5Y8adCy4Iwpxy7pQumQ/psnRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/utils/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/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==", - "dev": true, - "license": "MIT", - "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/@vitest/utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "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.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.25.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", - "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001733", - "electron-to-chromium": "^1.5.199", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001734", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", - "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "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==", - "dev": true, - "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", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/ci-info": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, - "node_modules/date-fns-tz": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.1.tgz", - "integrity": "sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA==", - "license": "MIT", - "peerDependencies": { - "date-fns": "2.x" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "license": "MIT" - }, - "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.199", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", - "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", - "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.20", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", - "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/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/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, - "node_modules/expect": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", - "integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "30.0.5", - "@jest/get-type": "30.0.1", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-equals": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", - "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": true, - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/goober": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", - "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", - "license": "MIT", - "peerDependencies": { - "csstype": "^3.0.10" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", - "license": "MIT", - "dependencies": { - "void-elements": "3.1.0" - } - }, - "node_modules/i18next": { - "version": "23.16.8", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", - "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==", - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - } - ], - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.2" - } - }, - "node_modules/i18next-browser-languagedetector": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.2.tgz", - "integrity": "sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.2" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-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==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jest-diff": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", - "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.0.1", - "chalk": "^4.1.2", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-diff/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/jest-diff/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-diff/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-matcher-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", - "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1", - "chalk": "^4.1.2", - "jest-diff": "30.0.5", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-matcher-utils/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/jest-matcher-utils/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-message-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", - "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.0.5", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.0.5", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-message-util/node_modules/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/jest-message-util/node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-message-util/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-mock": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", - "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", - "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-util/node_modules/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/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.castarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", - "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.263.1", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.263.1.tgz", - "integrity": "sha512-keqxAx97PlaEN89PXZ6ki1N8nRjGWtDa4021GFYLNj0RgruM5odbpl8GHTExj0hhPq3sF6Up0gnxt6TSHu+ovw==", - "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==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mini-svg-data-uri": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", - "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", - "dev": true, - "license": "MIT", - "bin": { - "mini-svg-data-uri": "cli.js" - } - }, - "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "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/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mlly": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", - "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.14.0", - "pathe": "^2.0.1", - "pkg-types": "^1.3.0", - "ufo": "^1.5.4" - } - }, - "node_modules/mlly/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/pkg-types/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "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==", - "dev": true, - "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", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "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", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "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-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dev": true, - "license": "MIT", - "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", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-hook-form": { - "version": "7.62.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", - "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-hook-form" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18 || ^19" - } - }, - "node_modules/react-hot-toast": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", - "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", - "license": "MIT", - "dependencies": { - "csstype": "^3.1.3", - "goober": "^2.1.16" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": ">=16", - "react-dom": ">=16" - } - }, - "node_modules/react-i18next": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.5.0.tgz", - "integrity": "sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.22.5", - "html-parse-stringify": "^3.0.1" - }, - "peerDependencies": { - "i18next": ">= 23.2.3", - "react": ">= 16.8.0" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-redux": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", - "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.1", - "@types/hoist-non-react-statics": "^3.3.1", - "@types/use-sync-external-store": "^0.0.3", - "hoist-non-react-statics": "^3.3.2", - "react-is": "^18.0.0", - "use-sync-external-store": "^1.0.0" - }, - "peerDependencies": { - "@types/react": "^16.8 || ^17.0 || ^18.0", - "@types/react-dom": "^16.8 || ^17.0 || ^18.0", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0", - "react-native": ">=0.59", - "redux": "^4 || ^5.0.0-beta.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/react-redux/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==", - "license": "MIT" - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", - "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", - "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "license": "MIT", - "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/recharts": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", - "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", - "license": "MIT", - "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "license": "MIT", - "dependencies": { - "decimal.js-light": "^2.4.1" - } - }, - "node_modules/recharts/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==", - "license": "MIT" - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, - "node_modules/redux-thunk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", - "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", - "license": "MIT", - "peerDependencies": { - "redux": "^4" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reselect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", - "dev": true, - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", - "dev": true, - "license": "MIT" - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "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/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwind-merge": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", - "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinypool": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", - "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", - "license": "MIT AND ISC", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, - "node_modules/vite": { - "version": "4.5.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", - "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "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/vite-node": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", - "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "mlly": "^1.4.0", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": ">=v14.18.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", - "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^4.3.5", - "@types/chai-subset": "^1.3.3", - "@types/node": "*", - "@vitest/expect": "0.34.6", - "@vitest/runner": "0.34.6", - "@vitest/snapshot": "0.34.6", - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "acorn": "^8.9.0", - "acorn-walk": "^8.2.0", - "cac": "^6.7.14", - "chai": "^4.3.10", - "debug": "^4.3.4", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.3.3", - "strip-literal": "^1.0.1", - "tinybench": "^2.5.0", - "tinypool": "^0.7.0", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", - "vite-node": "0.34.6", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": ">=v14.18.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@vitest/browser": "*", - "@vitest/ui": "*", - "happy-dom": "*", - "jsdom": "*", - "playwright": "*", - "safaridriver": "*", - "webdriverio": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "playwright": { - "optional": true - }, - "safaridriver": { - "optional": true - }, - "webdriverio": { - "optional": true - } - } - }, - "node_modules/vitest/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/vitest/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/vitest/node_modules/@vitest/utils": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", - "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/vitest/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==", - "dev": true, - "license": "MIT", - "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/vitest/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "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", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/fdev-ffrontend/package.json b/fdev-ffrontend/package.json deleted file mode 100644 index 76b7241c..00000000 --- a/fdev-ffrontend/package.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "name": "pania-frontend", - "version": "1.0.0", - "description": "AI-powered bakery demand forecasting platform for Madrid", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "test": "vitest", - "test:ui": "vitest --ui", - "test:coverage": "vitest --coverage", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "lint:fix": "eslint . --ext ts,tsx --fix" - }, - "dependencies": { - "@hookform/resolvers": "^3.3.1", - "@reduxjs/toolkit": "^1.9.5", - "@stripe/react-stripe-js": "^3.9.0", - "@stripe/stripe-js": "^7.8.0", - "clsx": "^2.0.0", - "date-fns": "^2.30.0", - "date-fns-tz": "^2.0.0", - "i18next": "^23.4.4", - "i18next-browser-languagedetector": "^7.1.0", - "lucide-react": "^0.263.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-hook-form": "^7.45.4", - "react-hot-toast": "^2.4.1", - "react-i18next": "^13.1.2", - "react-redux": "^8.1.2", - "react-router-dom": "^6.15.0", - "recharts": "^2.8.0", - "tailwind-merge": "^1.14.0", - "zod": "^3.22.2" - }, - "devDependencies": { - "@tailwindcss/forms": "^0.5.4", - "@tailwindcss/typography": "^0.5.9", - "@testing-library/jest-dom": "^5.17.0", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "@types/react": "^18.2.15", - "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "@vitejs/plugin-react": "^4.0.3", - "@vitest/ui": "^0.34.1", - "autoprefixer": "^10.4.14", - "eslint": "^8.45.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", - "postcss": "^8.4.27", - "tailwindcss": "^3.3.0", - "typescript": "^5.0.2", - "vite": "^4.4.5", - "vitest": "^0.34.1" - }, - "keywords": [ - "bakery", - "forecasting", - "ai", - "madrid", - "react", - "typescript", - "tailwind" - ], - "repository": { - "type": "git", - "url": "https://github.com/pania-es/frontend" - }, - "author": "PanIA Team", - "license": "MIT" -} diff --git a/fdev-ffrontend/postcss.config.js b/fdev-ffrontend/postcss.config.js deleted file mode 100644 index e99ebc2c..00000000 --- a/fdev-ffrontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} \ No newline at end of file diff --git a/fdev-ffrontend/src/App.tsx b/fdev-ffrontend/src/App.tsx deleted file mode 100644 index 5ccda2ad..00000000 --- a/fdev-ffrontend/src/App.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { useEffect } from 'react'; -import { RouterProvider } from 'react-router-dom'; -import { Provider } from 'react-redux'; -import { Toaster } from 'react-hot-toast'; -import { router } from './router'; -import { store } from './store'; -import ErrorBoundary from './components/ErrorBoundary'; -import { useAuth } from './hooks/useAuth'; - -// i18n -import './i18n'; - -// Global styles -import './styles/globals.css'; - -const AppContent: React.FC = () => { - const { initializeAuth } = useAuth(); - - useEffect(() => { - initializeAuth(); - }, [initializeAuth]); - - return ( - -
- - - {/* Global Toast Notifications */} - -
-
- ); -}; - -const App: React.FC = () => { - return ( - - - - ); -}; - -export default App; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/README.md b/fdev-ffrontend/src/api/README.md deleted file mode 100644 index 45a6e599..00000000 --- a/fdev-ffrontend/src/api/README.md +++ /dev/null @@ -1,85 +0,0 @@ -frontend/src/api/ -├── client/ # HTTP client configuration -│ ├── index.ts # Main API client -│ ├── config.ts # Client configuration -│ ├── interceptors.ts # Request/response interceptors -│ └── types.ts # Client-specific types -├── services/ # Service-specific API calls -│ ├── index.ts # Export all services -│ ├── auth.service.ts # Authentication operations -│ ├── tenant.service.ts # Tenant management -│ ├── data.service.ts # Data operations -│ ├── training.service.ts # ML training operations -│ ├── forecasting.service.ts # Forecasting operations -│ └── notification.service.ts # Notification operations -├── types/ # TypeScript definitions -│ ├── index.ts # Re-export all types -│ ├── common.ts # Common API types -│ ├── auth.ts # Authentication types -│ ├── tenant.ts # Tenant types -│ ├── data.ts # Data types -│ ├── training.ts # Training types -│ ├── forecasting.ts # Forecasting types -│ └── notification.ts # Notification types -├── hooks/ # React hooks for API calls -│ ├── index.ts # Export all hooks -│ ├── useAuth.ts # Authentication hooks -│ ├── useTenant.ts # Tenant hooks -│ ├── useData.ts # Data hooks -│ ├── useTraining.ts # Training hooks -│ ├── useForecast.ts # Forecasting hooks -│ └── useNotification.ts # Notification hooks -├── utils/ # API utilities -│ ├── index.ts # Export utilities -│ ├── response.ts # Response handling -│ ├── error.ts # Error handling -│ ├── validation.ts # Request validation -│ └── transform.ts # Data transformation -├── websocket/ # WebSocket management -│ ├── index.ts # WebSocket exports -│ ├── manager.ts # WebSocket manager -│ ├── types.ts # WebSocket types -│ └── hooks.ts # WebSocket hooks -└── index.ts # Main API exports -``` - -## 🎯 Key Improvements - -### 1. **Modern Architecture Patterns** -- **Service Layer Pattern**: Clean separation of concerns -- **Repository Pattern**: Consistent data access layer -- **Factory Pattern**: Flexible service instantiation -- **Observer Pattern**: Event-driven updates - -### 2. **Type Safety** -- **Strict TypeScript**: Full type coverage -- **Schema Validation**: Runtime type checking -- **Generic Types**: Reusable type definitions -- **Union Types**: Precise API responses - -### 3. **Error Handling** -- **Centralized Error Management**: Consistent error handling -- **Error Recovery**: Automatic retry mechanisms -- **User-Friendly Messages**: Localized error messages -- **Error Boundaries**: Component-level error isolation - -### 4. **Performance Optimization** -- **Request Caching**: Intelligent cache management -- **Request Deduplication**: Prevent duplicate calls -- **Optimistic Updates**: Immediate UI feedback -- **Background Sync**: Offline-first approach - -### 5. **Developer Experience** -- **Auto-completion**: Full IntelliSense support -- **Type-safe Hooks**: React hooks with types -- **Error Prevention**: Compile-time error detection -- **Documentation**: Comprehensive JSDoc comments - -## 🚀 Implementation Benefits - -1. **Maintainability**: Modular structure for easy updates -2. **Scalability**: Easy to add new services and endpoints -3. **Testability**: Isolated services for unit testing -4. **Reusability**: Shared utilities and types -5. **Type Safety**: Prevent runtime errors -6. **Developer Productivity**: IntelliSense and auto-completion \ No newline at end of file diff --git a/fdev-ffrontend/src/api/client/config.ts b/fdev-ffrontend/src/api/client/config.ts deleted file mode 100644 index 0b4b9c8f..00000000 --- a/fdev-ffrontend/src/api/client/config.ts +++ /dev/null @@ -1,142 +0,0 @@ -// frontend/src/api/client/config.ts -/** - * API Client Configuration - * Centralized configuration for all API clients - */ - -export interface ApiConfig { - baseURL: string; - timeout: number; - retries: number; - retryDelay: number; - enableLogging: boolean; - enableCaching: boolean; - cacheTimeout: number; -} - -export interface ServiceEndpoints { - auth: string; - tenant: string; - data: string; - training: string; - forecasting: string; - notification: string; -} - -// Environment-based configuration -const getEnvironmentConfig = (): ApiConfig => { - // Use import.meta.env instead of process.env for Vite - const isDevelopment = import.meta.env.DEV; - const isProduction = import.meta.env.PROD; - - return { - baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1', - timeout: parseInt(import.meta.env.VITE_API_TIMEOUT || '30000'), - retries: parseInt(import.meta.env.VITE_API_RETRIES || '3'), - retryDelay: parseInt(import.meta.env.VITE_API_RETRY_DELAY || '1000'), - enableLogging: isDevelopment || import.meta.env.VITE_API_LOGGING === 'true', - enableCaching: import.meta.env.VITE_API_CACHING !== 'false', - cacheTimeout: parseInt(import.meta.env.VITE_API_CACHE_TIMEOUT || '300000'), // 5 minutes - }; -}; - -export const apiConfig: ApiConfig = getEnvironmentConfig(); - -// Service endpoint configuration -export const serviceEndpoints: ServiceEndpoints = { - auth: '/auth', - tenant: '/tenants', - data: '/tenants', // Data operations are tenant-scoped - training: '/tenants', // Training operations are tenant-scoped - forecasting: '/tenants', // Forecasting operations are tenant-scoped - notification: '/tenants', // Notification operations are tenant-scoped -}; - -// HTTP status codes -export const HttpStatus = { - OK: 200, - CREATED: 201, - NO_CONTENT: 204, - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - CONFLICT: 409, - UNPROCESSABLE_ENTITY: 422, - INTERNAL_SERVER_ERROR: 500, - BAD_GATEWAY: 502, - SERVICE_UNAVAILABLE: 503, -} as const; - -// Request timeout configuration -export const RequestTimeouts = { - SHORT: 5000, // 5 seconds - for quick operations - MEDIUM: 15000, // 15 seconds - for normal operations - LONG: 60000, // 1 minute - for file uploads - EXTENDED: 300000, // 5 minutes - for training operations -} as const; - -// Cache configuration -export interface CacheConfig { - defaultTTL: number; - maxSize: number; - strategies: { - user: number; - tenant: number; - data: number; - forecast: number; - }; -} - -export const cacheConfig: CacheConfig = { - defaultTTL: 300000, // 5 minutes - maxSize: 100, // Maximum cached items - strategies: { - user: 600000, // 10 minutes - tenant: 1800000, // 30 minutes - data: 300000, // 5 minutes - forecast: 600000, // 10 minutes - }, -}; - -// Retry configuration -export interface RetryConfig { - attempts: number; - delay: number; - backoff: number; - retryCondition: (error: any) => boolean; -} - -export const retryConfig: RetryConfig = { - attempts: 3, - delay: 1000, - backoff: 2, // Exponential backoff multiplier - retryCondition: (error: any) => { - // Retry on network errors and specific HTTP status codes - if (!error.response) return true; // Network error - const status = error.response.status; - return status >= 500 || status === 408 || status === 429; - }, -}; - -// API versioning -export const ApiVersion = { - V1: 'v1', - CURRENT: 'v1', -} as const; - -export interface FeatureFlags { - enableWebSockets: boolean; - enableOfflineMode: boolean; - enableOptimisticUpdates: boolean; - enableRequestDeduplication: boolean; - enableMetrics: boolean; -} - -export const featureFlags: FeatureFlags = { - enableWebSockets: import.meta.env.VITE_ENABLE_WEBSOCKETS === 'true', - enableOfflineMode: import.meta.env.VITE_ENABLE_OFFLINE === 'true', - enableOptimisticUpdates: import.meta.env.VITE_ENABLE_OPTIMISTIC_UPDATES !== 'false', - enableRequestDeduplication: import.meta.env.VITE_ENABLE_DEDUPLICATION !== 'false', - enableMetrics: import.meta.env.VITE_ENABLE_METRICS === 'true', -}; diff --git a/fdev-ffrontend/src/api/client/index.ts b/fdev-ffrontend/src/api/client/index.ts deleted file mode 100644 index 23959b2b..00000000 --- a/fdev-ffrontend/src/api/client/index.ts +++ /dev/null @@ -1,578 +0,0 @@ -// frontend/src/api/client/index.ts -/** - * Enhanced API Client with modern features - * Supports caching, retries, optimistic updates, and more - */ - -import { - ApiResponse, - ApiError, - RequestConfig, - UploadConfig, - UploadProgress, - RequestInterceptor, - ResponseInterceptor, - CacheEntry, - RequestMetrics, -} from './types'; -import { apiConfig, retryConfig, cacheConfig, featureFlags } from './config'; - -export class ApiClient { - private baseURL: string; - private cache = new Map(); - private pendingRequests = new Map>(); - private requestInterceptors: RequestInterceptor[] = []; - private responseInterceptors: ResponseInterceptor[] = []; - private metrics: RequestMetrics[] = []; - -constructor(baseURL?: string) { - this.baseURL = baseURL || apiConfig.baseURL; - // ✅ CRITICAL FIX: Remove trailing slash - this.baseURL = this.baseURL.replace(/\/+$/, ''); - console.log('🔧 API Client initialized with baseURL:', this.baseURL); -} - -private buildURL(endpoint: string): string { - // Remove leading slash from endpoint if present to avoid double slashes - const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; - const fullURL = `${this.baseURL}${cleanEndpoint}`; - - // ✅ DEBUG: Log URL construction - console.log('🔗 Building URL:', { - baseURL: this.baseURL, - endpoint: cleanEndpoint, - fullURL: fullURL - }); - - return fullURL; - } - - /** - * Add request interceptor - */ - addRequestInterceptor(interceptor: RequestInterceptor): void { - this.requestInterceptors.push(interceptor); - } - - /** - * Add response interceptor - */ - addResponseInterceptor(interceptor: ResponseInterceptor): void { - this.responseInterceptors.push(interceptor); - } - - /** - * Generate cache key for request - */ - private getCacheKey(url: string, config?: RequestConfig): string { - const method = config?.method || 'GET'; - const params = config?.params ? JSON.stringify(config.params) : ''; - return `${method}:${url}:${params}`; - } - - /** - * Check if response is cached and valid - */ - private getCachedResponse(key: string): T | null { - if (!featureFlags.enableRequestDeduplication && !apiConfig.enableCaching) { - return null; - } - - const cached = this.cache.get(key); - if (!cached) return null; - - const now = Date.now(); - if (now - cached.timestamp > cached.ttl) { - this.cache.delete(key); - return null; - } - - return cached.data; - } - - /** - * Cache response data - */ - private setCachedResponse(key: string, data: T, ttl?: number): void { - if (!apiConfig.enableCaching) return; - - const cacheTTL = ttl || cacheConfig.defaultTTL; - this.cache.set(key, { - data, - timestamp: Date.now(), - ttl: cacheTTL, - key, - }); - - // Cleanup old cache entries if cache is full - if (this.cache.size > cacheConfig.maxSize) { - const oldestKey = this.cache.keys().next().value; - this.cache.delete(oldestKey); - } - } - - /** - * Apply request interceptors - */ - private async applyRequestInterceptors(config: RequestConfig): Promise { - let modifiedConfig = { ...config }; - - for (const interceptor of this.requestInterceptors) { - if (interceptor.onRequest) { - try { - modifiedConfig = await interceptor.onRequest(modifiedConfig); - } catch (error) { - if (interceptor.onRequestError) { - await interceptor.onRequestError(error); - } - throw error; - } - } - } - - return modifiedConfig; - } - - /** - * Apply response interceptors - */ - private async applyResponseInterceptors(response: ApiResponse): Promise> { - let modifiedResponse = { ...response }; - - for (const interceptor of this.responseInterceptors) { - if (interceptor.onResponse) { - try { - modifiedResponse = await interceptor.onResponse(modifiedResponse); - } catch (error) { - if (interceptor.onResponseError) { - await interceptor.onResponseError(error); - } - throw error; - } - } - } - - return modifiedResponse; - } - - /** - * Retry failed requests with exponential backoff - */ - private async retryRequest( - requestFn: () => Promise, - attempts: number = retryConfig.attempts - ): Promise { - try { - return await requestFn(); - } catch (error) { - if (attempts <= 0 || !retryConfig.retryCondition(error)) { - throw error; - } - - const delay = retryConfig.delay * Math.pow(retryConfig.backoff, retryConfig.attempts - attempts); - await new Promise(resolve => setTimeout(resolve, delay)); - - return this.retryRequest(requestFn, attempts - 1); - } - } - - /** - * Record request metrics - */ - private recordMetrics(metrics: Partial): void { - if (!featureFlags.enableMetrics) return; - - const completeMetrics: RequestMetrics = { - url: '', - method: 'GET', - duration: 0, - status: 0, - size: 0, - timestamp: Date.now(), - cached: false, - retries: 0, - ...metrics, - }; - - this.metrics.push(completeMetrics); - - // Keep only recent metrics (last 1000 requests) - if (this.metrics.length > 1000) { - this.metrics = this.metrics.slice(-1000); - } - } - - /** - * Core request method with all features - */ - async request(endpoint: string, config: RequestConfig = {}): Promise { - const startTime = Date.now(); - const url = this.buildURL(endpoint); - const method = config.method || 'GET'; - - console.log('🚀 Making API request:', { - method, - endpoint, - url, - config - }); - - // Apply request interceptors - const modifiedConfig = await this.applyRequestInterceptors(config); - - // Generate cache key - const cacheKey = this.getCacheKey(endpoint, modifiedConfig); - - // Check cache for GET requests - if (method === 'GET' && (config.cache !== false)) { - const cached = this.getCachedResponse(cacheKey); - if (cached) { - this.recordMetrics({ - url: endpoint, - method, - duration: Date.now() - startTime, - status: 200, - cached: true, - }); - return cached; - } - } - - // Request deduplication for concurrent requests - if (featureFlags.enableRequestDeduplication && method === 'GET') { - const pendingRequest = this.pendingRequests.get(cacheKey); - if (pendingRequest) { - return pendingRequest; - } - } - - // Create request promise - const requestPromise = this.retryRequest(async () => { - const headers: Record = { - 'Content-Type': 'application/json', - ...modifiedConfig.headers, - }; - - const fetchConfig: RequestInit = { - method, - headers, - signal: AbortSignal.timeout(modifiedConfig.timeout || apiConfig.timeout), - }; - - // Add body for non-GET requests - if (method !== 'GET' && modifiedConfig.body) { - if (modifiedConfig.body instanceof FormData) { - // Remove Content-Type for FormData (let browser set it with boundary) - delete headers['Content-Type']; - fetchConfig.body = modifiedConfig.body; - } else { - fetchConfig.body = typeof modifiedConfig.body === 'string' - ? modifiedConfig.body - : JSON.stringify(modifiedConfig.body); - } - } - - // Add query parameters - const urlWithParams = new URL(url); - if (modifiedConfig.params) { - Object.entries(modifiedConfig.params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - urlWithParams.searchParams.append(key, String(value)); - } - }); - } - - const response = await fetch(urlWithParams.toString(), fetchConfig); - - if (!response.ok) { - const errorText = await response.text(); - let errorData: ApiError; - - try { - errorData = JSON.parse(errorText); - } catch { - errorData = { - message: `HTTP ${response.status}: ${response.statusText}`, - detail: errorText, - code: `HTTP_${response.status}`, - }; - } - - const error = new Error(errorData.message || 'Request failed'); - (error as any).response = { status: response.status, data: errorData }; - throw error; - } - - const responseData = await response.json(); - console.log('🔍 Raw responseData from fetch:', responseData); - - // Apply response interceptors - const processedResponse = await this.applyResponseInterceptors(responseData); - console.log('🔍 processedResponse after interceptors:', processedResponse); - - return processedResponse; - }); - - // Store pending request for deduplication - if (featureFlags.enableRequestDeduplication && method === 'GET') { - this.pendingRequests.set(cacheKey, requestPromise); - } - - try { - const result = await requestPromise; - - // Cache successful GET responses - if (method === 'GET' && config.cache !== false) { - this.setCachedResponse(cacheKey, result, config.cacheTTL); - } - - // Record metrics - this.recordMetrics({ - url: endpoint, - method, - duration: Date.now() - startTime, - status: 200, - size: JSON.stringify(result).length, - }); - - // Handle both wrapped and unwrapped responses - // If result has a 'data' property, return it; otherwise return the result itself - console.log('🔍 Final result before return:', result); - console.log('🔍 Result has data property?', result && typeof result === 'object' && 'data' in result); - - if (result && typeof result === 'object' && 'data' in result) { - console.log('🔍 Returning result.data:', result.data); - return result.data as T; - } - console.log('🔍 Returning raw result:', result); - return result as T; - } catch (error) { - // Record error metrics - this.recordMetrics({ - url: endpoint, - method, - duration: Date.now() - startTime, - status: (error as any).response?.status || 0, - }); - - throw error; - } finally { - // Clean up pending request - if (featureFlags.enableRequestDeduplication && method === 'GET') { - this.pendingRequests.delete(cacheKey); - } - } - } - - /** - * Convenience methods for HTTP verbs - */ - async get(endpoint: string, config?: RequestConfig): Promise { - return this.request(endpoint, { ...config, method: 'GET' }); - } - - async post(endpoint: string, data?: any, config?: RequestConfig): Promise { - return this.request(endpoint, { - ...config, - method: 'POST', - body: data - }); - } - - async put(endpoint: string, data?: any, config?: RequestConfig): Promise { - return this.request(endpoint, { - ...config, - method: 'PUT', - body: data - }); - } - - async patch(endpoint: string, data?: any, config?: RequestConfig): Promise { - return this.request(endpoint, { - ...config, - method: 'PATCH', - body: data - }); - } - - async delete(endpoint: string, config?: RequestConfig): Promise { - return this.request(endpoint, { ...config, method: 'DELETE' }); - } - - /** - * Raw request that returns the Response object for binary data - */ - async getRaw(endpoint: string, config?: RequestConfig): Promise { - const url = this.buildURL(endpoint); - const modifiedConfig = await this.applyRequestInterceptors(config || {}); - - const headers: Record = { - ...modifiedConfig.headers, - }; - - const fetchConfig: RequestInit = { - method: 'GET', - headers, - signal: AbortSignal.timeout(modifiedConfig.timeout || apiConfig.timeout), - }; - - // Add query parameters - const urlWithParams = new URL(url); - if (modifiedConfig.params) { - Object.entries(modifiedConfig.params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - urlWithParams.searchParams.append(key, String(value)); - } - }); - } - - const response = await fetch(urlWithParams.toString(), fetchConfig); - - if (!response.ok) { - const errorText = await response.text(); - let errorData: ApiError; - - try { - errorData = JSON.parse(errorText); - } catch { - errorData = { - message: `HTTP ${response.status}: ${response.statusText}`, - detail: errorText, - code: `HTTP_${response.status}`, - }; - } - - const error = new Error(errorData.message || 'Request failed'); - (error as any).response = { status: response.status, data: errorData }; - throw error; - } - - return response; - } - - /** - * File upload with progress tracking - */ - async upload( - endpoint: string, - file: File, - additionalData?: Record, - config?: UploadConfig - ): Promise { - const formData = new FormData(); - formData.append('file', file); - - if (additionalData) { - Object.entries(additionalData).forEach(([key, value]) => { - formData.append(key, String(value)); - }); - } - - // For file uploads, we need to use XMLHttpRequest for progress tracking - if (config?.onProgress) { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - - xhr.upload.addEventListener('progress', (event) => { - if (event.lengthComputable && config.onProgress) { - const progress: UploadProgress = { - loaded: event.loaded, - total: event.total, - percentage: Math.round((event.loaded / event.total) * 100), - }; - config.onProgress(progress); - } - }); - - xhr.addEventListener('load', () => { - if (xhr.status >= 200 && xhr.status < 300) { - try { - const result = JSON.parse(xhr.responseText); - resolve(result); - } catch { - resolve(xhr.responseText as any); - } - } else { - reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`)); - } - }); - - xhr.addEventListener('error', () => { - reject(new Error('Upload failed')); - }); - - xhr.open('POST', `${this.baseURL}${endpoint}`); - - // Add headers (excluding Content-Type for FormData) - if (config?.headers) { - Object.entries(config.headers).forEach(([key, value]) => { - if (key.toLowerCase() !== 'content-type') { - xhr.setRequestHeader(key, value); - } - }); - } - - xhr.send(formData); - }); - } - - // Fallback to regular request for uploads without progress - return this.request(endpoint, { - ...config, - method: 'POST', - body: formData, - }); - } - - /** - * Clear cache - */ - clearCache(pattern?: string): void { - if (pattern) { - // Clear cache entries matching pattern - const regex = new RegExp(pattern); - Array.from(this.cache.keys()) - .filter(key => regex.test(key)) - .forEach(key => this.cache.delete(key)); - } else { - // Clear all cache - this.cache.clear(); - } - } - - /** - * Get client metrics - */ - getMetrics() { - if (!featureFlags.enableMetrics) { - return { - totalRequests: 0, - successfulRequests: 0, - failedRequests: 0, - averageResponseTime: 0, - cacheHitRate: 0, - errorRate: 0, - }; - } - - const total = this.metrics.length; - const successful = this.metrics.filter(m => m.status >= 200 && m.status < 300).length; - const cached = this.metrics.filter(m => m.cached).length; - const averageTime = total > 0 - ? this.metrics.reduce((sum, m) => sum + m.duration, 0) / total - : 0; - - return { - totalRequests: total, - successfulRequests: successful, - failedRequests: total - successful, - averageResponseTime: Math.round(averageTime), - cacheHitRate: total > 0 ? Math.round((cached / total) * 100) : 0, - errorRate: total > 0 ? Math.round(((total - successful) / total) * 100) : 0, - }; - } -} - -// Default API client instance -console.log('🔧 Creating default API client...'); -export const apiClient = new ApiClient(); \ No newline at end of file diff --git a/fdev-ffrontend/src/api/client/interceptors.ts b/fdev-ffrontend/src/api/client/interceptors.ts deleted file mode 100644 index 59c87a11..00000000 --- a/fdev-ffrontend/src/api/client/interceptors.ts +++ /dev/null @@ -1,488 +0,0 @@ -// frontend/src/api/client/interceptors.ts -/** - * Request and Response Interceptors - * Handles authentication, logging, error handling, etc. - */ - -import { apiClient } from './index'; -import type { RequestConfig, ApiResponse } from './types'; -import { ApiErrorHandler } from '../utils'; - -/** - * Authentication Interceptor - * Automatically adds authentication headers to requests - */ -class AuthInterceptor { - static isTokenExpired(token: string): boolean { - try { - const payload = JSON.parse(atob(token.split('.')[1])); - const currentTime = Math.floor(Date.now() / 1000); - return payload.exp <= currentTime; - } catch (error) { - console.warn('Error parsing token:', error); - return true; // Treat invalid tokens as expired - } - } - - static isTokenExpiringSoon(token: string, bufferMinutes: number = 5): boolean { - try { - const payload = JSON.parse(atob(token.split('.')[1])); - const currentTime = Math.floor(Date.now() / 1000); - const bufferSeconds = bufferMinutes * 60; - return payload.exp <= (currentTime + bufferSeconds); - } catch (error) { - console.warn('Error parsing token for expiration check:', error); - return true; - } - } - - static async refreshTokenIfNeeded(): Promise { - const token = localStorage.getItem('auth_token'); - const refreshToken = localStorage.getItem('refresh_token'); - - if (!token || !refreshToken) { - return; - } - - // If token is expiring within 5 minutes, proactively refresh - if (this.isTokenExpiringSoon(token, 5)) { - try { - const baseURL = (apiClient as any).baseURL || window.location.origin; - const response = await fetch(`${baseURL}/api/v1/auth/refresh`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refresh_token: refreshToken }), - }); - - if (response.ok) { - const data = await response.json(); - localStorage.setItem('auth_token', data.access_token); - if (data.refresh_token) { - localStorage.setItem('refresh_token', data.refresh_token); - } - } else { - console.warn('Token refresh failed:', response.status); - } - } catch (error) { - console.warn('Token refresh error:', error); - } - } - } - - static setup() { - apiClient.addRequestInterceptor({ - onRequest: async (config: RequestConfig) => { - // Proactively refresh token if needed - await this.refreshTokenIfNeeded(); - - let token = localStorage.getItem('auth_token'); - - if (token) { - // Check if token is expired - if (this.isTokenExpired(token)) { - console.warn('Token expired, removing from storage'); - localStorage.removeItem('auth_token'); - token = null; - } - } - - if (token) { - config.headers = { - ...config.headers, - Authorization: `Bearer ${token}`, - }; - } else { - console.warn('No valid auth token found - authentication required'); - } - - return config; - }, - - onRequestError: async (error: any) => { - console.error('Request interceptor error:', error); - throw error; - }, - }); - } -} - -/** - * Logging Interceptor - * Logs API requests and responses for debugging - */ -class LoggingInterceptor { - static setup() { - apiClient.addRequestInterceptor({ - onRequest: async (config: RequestConfig) => { - const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - console.group(`🚀 API Request [${requestId}]`); - console.log('Method:', config.method); - console.log('URL:', config.url); - console.log('Headers:', config.headers); - if (config.body && config.method !== 'GET') { - console.log('Body:', config.body); - } - if (config.params) { - console.log('Params:', config.params); - } - console.groupEnd(); - - // Add request ID to config for response correlation - config.headers = { - ...config.headers, - 'X-Request-ID': requestId, - }; - - return config; - }, - }); - - apiClient.addResponseInterceptor({ - onResponse: async (response: ApiResponse) => { - const requestId = response.meta?.requestId || 'unknown'; - - console.group(`✅ API Response [${requestId}]`); - console.log('Status:', response.status); - console.log('Data:', response.data); - if (response.message) { - console.log('Message:', response.message); - } - console.groupEnd(); - - return response; - }, - - onResponseError: async (error: any) => { - const requestId = error?.config?.headers?.[`X-Request-ID`] || 'unknown'; - - console.group(`❌ API Error [${requestId}]`); - console.error('Status:', error?.response?.status); - console.error('Error:', ApiErrorHandler.formatError(error)); - console.error('Full Error:', error); - console.groupEnd(); - - throw error; - }, - }); - } -} - -/** - * Tenant Context Interceptor - * Automatically adds tenant context to tenant-scoped requests - */ -class TenantInterceptor { - private static currentTenantId: string | null = null; - - static setCurrentTenant(tenantId: string | null) { - this.currentTenantId = tenantId; - } - - static getCurrentTenant(): string | null { - return this.currentTenantId; - } - - static setup() { - apiClient.addRequestInterceptor({ - onRequest: async (config: RequestConfig) => { - // Add tenant context to tenant-scoped endpoints - if (this.currentTenantId && this.isTenantScopedEndpoint(config.url)) { - config.headers = { - ...config.headers, - 'X-Tenant-ID': this.currentTenantId, - }; - } - - return config; - }, - }); - } - - private static isTenantScopedEndpoint(url?: string): boolean { - if (!url) return false; - return url.includes('/tenants/') || - url.includes('/training/') || - url.includes('/forecasts/') || - url.includes('/notifications/'); - } -} - -/** - * Error Recovery Interceptor - * Handles automatic token refresh and retry logic - */ -class ErrorRecoveryInterceptor { - private static isRefreshing = false; - private static failedQueue: Array<{ - resolve: (token: string) => void; - reject: (error: any) => void; - }> = []; - - static setup() { - apiClient.addResponseInterceptor({ - onResponseError: async (error: any) => { - const originalRequest = error.config; - - // Handle 401 errors with token refresh - if (error?.response?.status === 401 && !originalRequest._retry) { - if (this.isRefreshing) { - // Queue the request while refresh is in progress - return new Promise((resolve, reject) => { - this.failedQueue.push({ resolve, reject }); - }).then(token => { - return this.retryRequestWithNewToken(originalRequest, token as string); - }).catch(err => { - throw err; - }); - } - - originalRequest._retry = true; - this.isRefreshing = true; - - try { - const refreshToken = localStorage.getItem('refresh_token'); - - if (!refreshToken) { - throw new Error('No refresh token available'); - } - - // Use direct fetch to avoid interceptor recursion - const baseURL = (apiClient as any).baseURL || window.location.origin; - const response = await fetch(`${baseURL}/api/v1/auth/refresh`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ refresh_token: refreshToken }), - }); - - if (!response.ok) { - throw new Error(`Token refresh failed: ${response.status}`); - } - - const data = await response.json(); - const newToken = data.access_token; - - if (!newToken) { - throw new Error('No access token received'); - } - - localStorage.setItem('auth_token', newToken); - - // Update new refresh token if provided - if (data.refresh_token) { - localStorage.setItem('refresh_token', data.refresh_token); - } - - // Process failed queue - this.processQueue(null, newToken); - - // Retry original request with new token - return this.retryRequestWithNewToken(originalRequest, newToken); - - } catch (refreshError) { - console.warn('Token refresh failed:', refreshError); - this.processQueue(refreshError, null); - - // Clear auth data and redirect to login - localStorage.removeItem('auth_token'); - localStorage.removeItem('refresh_token'); - localStorage.removeItem('user_data'); - - // Only redirect if we're not already on the login page - if (typeof window !== 'undefined' && !window.location.pathname.includes('/login')) { - window.location.href = '/login'; - } - - throw refreshError; - } finally { - this.isRefreshing = false; - } - } - - throw error; - }, - }); - } - - private static async retryRequestWithNewToken(originalRequest: any, token: string) { - try { - // Use direct fetch instead of apiClient to avoid interceptor recursion - const url = originalRequest.url || originalRequest.endpoint; - const method = originalRequest.method || 'GET'; - - const fetchOptions: RequestInit = { - method, - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - ...originalRequest.headers - } - }; - - // Add body for non-GET requests - if (method !== 'GET' && originalRequest.body) { - fetchOptions.body = typeof originalRequest.body === 'string' - ? originalRequest.body - : JSON.stringify(originalRequest.body); - } - - // Add query parameters if present - let fullUrl = url; - if (originalRequest.params) { - const urlWithParams = new URL(fullUrl, (apiClient as any).baseURL); - Object.entries(originalRequest.params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - urlWithParams.searchParams.append(key, String(value)); - } - }); - fullUrl = urlWithParams.toString(); - } - - // Retry request with refreshed token - - const response = await fetch(fullUrl, fetchOptions); - - if (!response.ok) { - throw new Error(`Request failed: ${response.status}`); - } - - return await response.json(); - } catch (retryError) { - console.warn('Request retry failed:', retryError); - throw retryError; - } - } - - private static processQueue(error: any, token: string | null) { - this.failedQueue.forEach(({ resolve, reject }) => { - if (error) { - reject(error); - } else { - resolve(token!); - } - }); - - this.failedQueue = []; - } -} - -/** - * Performance Monitoring Interceptor - * Tracks API performance metrics - */ -class PerformanceInterceptor { - private static metrics: Array<{ - url: string; - method: string; - duration: number; - status: number; - timestamp: number; - }> = []; - - static setup() { - apiClient.addRequestInterceptor({ - onRequest: async (config: RequestConfig) => { - config.metadata = { - ...config.metadata, - startTime: Date.now(), - }; - - return config; - }, - }); - - apiClient.addResponseInterceptor({ - onResponse: async (response: ApiResponse) => { - const startTime = response.metadata?.startTime; - if (startTime) { - const duration = Date.now() - startTime; - this.recordMetric({ - url: response.metadata?.url || 'unknown', - method: response.metadata?.method || 'unknown', - duration, - status: 200, - timestamp: Date.now(), - }); - } - - return response; - }, - - onResponseError: async (error: any) => { - const startTime = error.config?.metadata?.startTime; - if (startTime) { - const duration = Date.now() - startTime; - this.recordMetric({ - url: error.config?.url || 'unknown', - method: error.config?.method || 'unknown', - duration, - status: error?.response?.status || 0, - timestamp: Date.now(), - }); - } - - throw error; - }, - }); - } - - private static recordMetric(metric: any) { - this.metrics.push(metric); - - // Keep only last 1000 metrics - if (this.metrics.length > 1000) { - this.metrics = this.metrics.slice(-1000); - } - } - - static getMetrics() { - return [...this.metrics]; - } - - static getAverageResponseTime(): number { - if (this.metrics.length === 0) return 0; - - const total = this.metrics.reduce((sum, metric) => sum + metric.duration, 0); - return Math.round(total / this.metrics.length); - } - - static getErrorRate(): number { - if (this.metrics.length === 0) return 0; - - const errorCount = this.metrics.filter(metric => metric.status >= 400).length; - return Math.round((errorCount / this.metrics.length) * 100); - } -} - -/** - * Setup all interceptors - * IMPORTANT: Order matters! ErrorRecoveryInterceptor must be first to handle token refresh - */ -export const setupInterceptors = () => { - // 1. Error recovery first (handles 401 and token refresh) - ErrorRecoveryInterceptor.setup(); - - // 2. Authentication (adds Bearer tokens) - AuthInterceptor.setup(); - - // 3. Tenant context - TenantInterceptor.setup(); - - // 4. Development-only interceptors - const isDevelopment = true; // Temporarily set to true for development - if (isDevelopment) { - LoggingInterceptor.setup(); - PerformanceInterceptor.setup(); - } -}; - -// Export interceptor classes for manual setup if needed -export { - AuthInterceptor, - LoggingInterceptor, - TenantInterceptor, - ErrorRecoveryInterceptor, - PerformanceInterceptor, -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/client/types.ts b/fdev-ffrontend/src/api/client/types.ts deleted file mode 100644 index 2b54af5f..00000000 --- a/fdev-ffrontend/src/api/client/types.ts +++ /dev/null @@ -1,115 +0,0 @@ -// frontend/src/api/client/types.ts -/** - * Core API Client Types - */ - -export interface RequestConfig { - method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; - headers?: Record; - params?: Record; - body?: any; - url?: string; - timeout?: number; - retries?: number; - cache?: boolean; - cacheTTL?: number; - optimistic?: boolean; - background?: boolean; - metadata?: any; -} - -export interface ApiResponse { - data: T; - message?: string; - status: string; - timestamp?: string; - metadata?: any; - meta?: { - page?: number; - limit?: number; - total?: number; - hasNext?: boolean; - hasPrev?: boolean; - requestId?: string; - }; -} - -export interface ApiError { - message: string; - detail?: string; - code?: string; - field?: string; - timestamp?: string; - service?: string; - requestId?: string; -} - -export interface PaginatedResponse { - data: T[]; - pagination: { - page: number; - limit: number; - total: number; - totalPages: number; - hasNext: boolean; - hasPrev: boolean; - }; -} - -export interface UploadProgress { - loaded: number; - total: number; - percentage: number; -} - -export interface UploadConfig extends RequestConfig { - onProgress?: (progress: UploadProgress) => void; - maxFileSize?: number; - allowedTypes?: string[]; -} - -// Request/Response interceptor types -export interface RequestInterceptor { - onRequest?: (config: RequestConfig) => RequestConfig | Promise; - onRequestError?: (error: any) => any; -} - -export interface ResponseInterceptor { - onResponse?: (response: ApiResponse) => ApiResponse | Promise>; - onResponseError?: (error: any) => any; -} - -// Cache types -export interface CacheEntry { - data: T; - timestamp: number; - ttl: number; - key: string; -} - -export interface CacheStrategy { - key: (url: string, params?: any) => string; - ttl: number; - enabled: boolean; -} - -// Metrics types -export interface RequestMetrics { - url: string; - method: string; - duration: number; - status: number; - size: number; - timestamp: number; - cached: boolean; - retries: number; -} - -export interface ClientMetrics { - totalRequests: number; - successfulRequests: number; - failedRequests: number; - averageResponseTime: number; - cacheHitRate: number; - errorRate: number; -} \ No newline at end of file diff --git a/fdev-ffrontend/src/api/hooks/index.ts b/fdev-ffrontend/src/api/hooks/index.ts deleted file mode 100644 index bb855104..00000000 --- a/fdev-ffrontend/src/api/hooks/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -// frontend/src/api/hooks/index.ts -/** - * Main Hooks Export - */ - -export { useAuth, useAuthHeaders } from './useAuth'; -export { useTenant } from './useTenant'; -export { useSales } from './useSales'; -export { useExternal } from './useExternal'; -export { useTraining } from './useTraining'; -export { useForecast } from './useForecast'; -export { useNotification } from './useNotification'; -export { useOnboarding, useOnboardingStep } from './useOnboarding'; -export { useInventory, useInventoryDashboard, useInventoryItem, useInventoryProducts } from './useInventory'; -export { useRecipes, useProduction } from './useRecipes'; -export { - useCurrentProcurementPlan, - useProcurementPlanByDate, - useProcurementPlan, - useProcurementPlans, - usePlanRequirements, - useCriticalRequirements, - useProcurementDashboard, - useGenerateProcurementPlan, - useUpdatePlanStatus, - useTriggerDailyScheduler, - useProcurementHealth, - useProcurementPlanDashboard, - useProcurementPlanActions -} from './useProcurement'; - -// Import hooks for combined usage -import { useAuth } from './useAuth'; -import { useTenant } from './useTenant'; -import { useSales } from './useSales'; -import { useExternal } from './useExternal'; -import { useTraining } from './useTraining'; -import { useForecast } from './useForecast'; -import { useNotification } from './useNotification'; -import { useOnboarding } from './useOnboarding'; -import { useInventory } from './useInventory'; - -// Combined hook for common operations -export const useApiHooks = () => { - const auth = useAuth(); - const tenant = useTenant(); - const sales = useSales(); - const external = useExternal(); - const training = useTraining({ disablePolling: true }); // Disable polling by default - const forecast = useForecast(); - const notification = useNotification(); - const onboarding = useOnboarding(); - const inventory = useInventory(); - - return { - auth, - tenant, - sales, - external, - training, - forecast, - notification, - onboarding, - inventory - }; -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/hooks/useAuth.ts b/fdev-ffrontend/src/api/hooks/useAuth.ts deleted file mode 100644 index 6cb6ba15..00000000 --- a/fdev-ffrontend/src/api/hooks/useAuth.ts +++ /dev/null @@ -1,205 +0,0 @@ -// frontend/src/api/hooks/useAuth.ts -/** - * Authentication Hooks - * React hooks for authentication operations - */ - -import { useState, useEffect, useCallback } from 'react'; -import { authService } from '../services'; -import type { - LoginRequest, - LoginResponse, - RegisterRequest, - UserResponse, - PasswordResetRequest, -} from '../types'; - -// Token management -const TOKEN_KEY = 'auth_token'; -const REFRESH_TOKEN_KEY = 'refresh_token'; -const USER_KEY = 'user_data'; - -export const useAuth = () => { - const [user, setUser] = useState(null); - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - // Initialize auth state from localStorage - useEffect(() => { - const initializeAuth = async () => { - try { - const token = localStorage.getItem(TOKEN_KEY); - const userData = localStorage.getItem(USER_KEY); - - if (token && userData) { - setUser(JSON.parse(userData)); - setIsAuthenticated(true); - - // Verify token is still valid - try { - const currentUser = await authService.getCurrentUser(); - setUser(currentUser); - } catch (error) { - // Token might be expired - let interceptors handle refresh - // Only logout if refresh also fails (handled by ErrorRecoveryInterceptor) - console.log('Token verification failed, interceptors will handle refresh if possible'); - - // Check if we have a refresh token - if not, logout immediately - const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY); - if (!refreshToken) { - console.log('No refresh token available, logging out'); - logout(); - } - } - } - } catch (error) { - console.error('Auth initialization error:', error); - logout(); - } finally { - setIsLoading(false); - } - }; - - initializeAuth(); - }, []); - - const login = useCallback(async (credentials: LoginRequest): Promise => { - try { - setIsLoading(true); - setError(null); - - const response = await authService.login(credentials); - - // Store tokens and user data - localStorage.setItem(TOKEN_KEY, response.access_token); - if (response.refresh_token) { - localStorage.setItem(REFRESH_TOKEN_KEY, response.refresh_token); - } - if (response.user) { - localStorage.setItem(USER_KEY, JSON.stringify(response.user)); - setUser(response.user); - } - - setIsAuthenticated(true); - } catch (error) { - const message = error instanceof Error ? error.message : 'Login failed'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const register = useCallback(async (data: RegisterRequest): Promise => { - try { - setIsLoading(true); - setError(null); - - const response = await authService.register(data); - - // Auto-login after successful registration - if (response && response.user) { - await login({ email: data.email, password: data.password }); - } else { - // If response doesn't have user property, registration might still be successful - // Try to login anyway in case the user was created but response format is different - await login({ email: data.email, password: data.password }); - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Registration failed'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, [login]); - - const logout = useCallback(async (): Promise => { - try { - // Call logout endpoint if authenticated - if (isAuthenticated) { - await authService.logout(); - } - } catch (error) { - console.error('Logout error:', error); - } finally { - // Clear local state regardless of API call success - localStorage.removeItem(TOKEN_KEY); - localStorage.removeItem(REFRESH_TOKEN_KEY); - localStorage.removeItem(USER_KEY); - setUser(null); - setIsAuthenticated(false); - setError(null); - } - }, [isAuthenticated]); - - const updateProfile = useCallback(async (data: Partial): Promise => { - try { - setIsLoading(true); - setError(null); - - const updatedUser = await authService.updateProfile(data); - setUser(updatedUser); - localStorage.setItem(USER_KEY, JSON.stringify(updatedUser)); - } catch (error) { - const message = error instanceof Error ? error.message : 'Profile update failed'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const requestPasswordReset = useCallback(async (data: PasswordResetRequest): Promise => { - try { - setIsLoading(true); - setError(null); - await authService.requestPasswordReset(data); - } catch (error) { - const message = error instanceof Error ? error.message : 'Password reset request failed'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const changePassword = useCallback(async (currentPassword: string, newPassword: string): Promise => { - try { - setIsLoading(true); - setError(null); - await authService.changePassword(currentPassword, newPassword); - } catch (error) { - const message = error instanceof Error ? error.message : 'Password change failed'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - return { - user, - isAuthenticated, - isLoading, - error, - login, - register, - logout, - updateProfile, - requestPasswordReset, - changePassword, - clearError: () => setError(null), - }; -}; - -// Hook for getting authentication headers -export const useAuthHeaders = () => { - const getAuthHeaders = useCallback(() => { - const token = localStorage.getItem(TOKEN_KEY); - return token ? { Authorization: `Bearer ${token}` } : {}; - }, []); - - return { getAuthHeaders }; -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/hooks/useExternal.ts b/fdev-ffrontend/src/api/hooks/useExternal.ts deleted file mode 100644 index c7d72aa3..00000000 --- a/fdev-ffrontend/src/api/hooks/useExternal.ts +++ /dev/null @@ -1,238 +0,0 @@ -// frontend/src/api/hooks/useExternal.ts -/** - * External Data Management Hooks - * Handles weather and traffic data operations - */ - -import { useState, useCallback } from 'react'; -import { externalService } from '../services/external.service'; -import type { WeatherData, TrafficData, WeatherForecast, HourlyForecast } from '../services/external.service'; - -export const useExternal = () => { - const [weatherData, setWeatherData] = useState(null); - const [trafficData, setTrafficData] = useState(null); - const [weatherForecast, setWeatherForecast] = useState([]); - const [hourlyForecast, setHourlyForecast] = useState([]); - const [trafficForecast, setTrafficForecast] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - /** - * Get Current Weather - */ - const getCurrentWeather = useCallback(async ( - tenantId: string, - lat: number, - lon: number - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const weather = await externalService.getCurrentWeather(tenantId, lat, lon); - setWeatherData(weather); - - return weather; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get weather data'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - /** - * Get Weather Forecast - */ - const getWeatherForecast = useCallback(async ( - tenantId: string, - lat: number, - lon: number, - days: number = 7 - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const forecast = await externalService.getWeatherForecast(tenantId, lat, lon, days); - setWeatherForecast(forecast); - - return forecast; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get weather forecast'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - /** - * Get Hourly Weather Forecast - */ - const getHourlyWeatherForecast = useCallback(async ( - tenantId: string, - lat: number, - lon: number, - hours: number = 48 - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const forecast = await externalService.getHourlyWeatherForecast(tenantId, lat, lon, hours); - setHourlyForecast(forecast); - - return forecast; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get hourly weather forecast'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - /** - * Get Historical Weather Data - */ - const getHistoricalWeather = useCallback(async ( - tenantId: string, - lat: number, - lon: number, - startDate: string, - endDate: string - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const data = await externalService.getHistoricalWeather(tenantId, lat, lon, startDate, endDate); - - return data; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get historical weather'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - /** - * Get Current Traffic - */ - const getCurrentTraffic = useCallback(async ( - tenantId: string, - lat: number, - lon: number - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const traffic = await externalService.getCurrentTraffic(tenantId, lat, lon); - setTrafficData(traffic); - - return traffic; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get traffic data'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - /** - * Get Traffic Forecast - */ - const getTrafficForecast = useCallback(async ( - tenantId: string, - lat: number, - lon: number, - hours: number = 24 - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const forecast = await externalService.getTrafficForecast(tenantId, lat, lon, hours); - setTrafficForecast(forecast); - - return forecast; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get traffic forecast'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - /** - * Get Historical Traffic Data - */ - const getHistoricalTraffic = useCallback(async ( - tenantId: string, - lat: number, - lon: number, - startDate: string, - endDate: string - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const data = await externalService.getHistoricalTraffic(tenantId, lat, lon, startDate, endDate); - - return data; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get historical traffic'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - /** - * Test External Services Connectivity - */ - const testConnectivity = useCallback(async (tenantId: string) => { - try { - setIsLoading(true); - setError(null); - - const results = await externalService.testConnectivity(tenantId); - - return results; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to test connectivity'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - return { - weatherData, - trafficData, - weatherForecast, - hourlyForecast, - trafficForecast, - isLoading, - error, - getCurrentWeather, - getWeatherForecast, - getHourlyWeatherForecast, - getHistoricalWeather, - getCurrentTraffic, - getTrafficForecast, - getHistoricalTraffic, - testConnectivity, - clearError: () => setError(null), - }; -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/hooks/useForecast.ts b/fdev-ffrontend/src/api/hooks/useForecast.ts deleted file mode 100644 index 2fa628a0..00000000 --- a/fdev-ffrontend/src/api/hooks/useForecast.ts +++ /dev/null @@ -1,229 +0,0 @@ -// frontend/src/api/hooks/useForecast.ts -/** - * Forecasting Operations Hooks - */ - -import { useState, useCallback } from 'react'; -import { forecastingService } from '../services'; -import type { - SingleForecastRequest, - BatchForecastRequest, - ForecastResponse, - BatchForecastResponse, - ForecastAlert, - QuickForecast, -} from '../types'; - -export const useForecast = () => { - const [forecasts, setForecasts] = useState([]); - const [batchForecasts, setBatchForecasts] = useState([]); - const [quickForecasts, setQuickForecasts] = useState([]); - const [alerts, setAlerts] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const createSingleForecast = useCallback(async ( - tenantId: string, - request: SingleForecastRequest - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const newForecasts = await forecastingService.createSingleForecast(tenantId, request); - setForecasts(prev => [...newForecasts, ...(prev || [])]); - - return newForecasts; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to create forecast'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const createBatchForecast = useCallback(async ( - tenantId: string, - request: BatchForecastRequest - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const batchForecast = await forecastingService.createBatchForecast(tenantId, request); - setBatchForecasts(prev => [batchForecast, ...(prev || [])]); - - return batchForecast; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to create batch forecast'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getForecasts = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const response = await forecastingService.getForecasts(tenantId); - setForecasts(response.data); - - return response.data; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get forecasts'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getBatchForecastStatus = useCallback(async ( - tenantId: string, - batchId: string - ): Promise => { - try { - const batchForecast = await forecastingService.getBatchForecastStatus(tenantId, batchId); - - // Update batch forecast in state - setBatchForecasts(prev => (prev || []).map(bf => - bf.id === batchId ? batchForecast : bf - )); - - return batchForecast; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get batch forecast status'; - setError(message); - throw error; - } - }, []); - - const getQuickForecasts = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const quickForecastData = await forecastingService.getQuickForecasts(tenantId); - setQuickForecasts(quickForecastData); - - return quickForecastData; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get quick forecasts'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getForecastAlerts = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const response = await forecastingService.getForecastAlerts(tenantId); - - // Handle different response formats - if (response && 'data' in response && response.data) { - // Standard paginated format: { data: [...], pagination: {...} } - setAlerts(response.data); - return { alerts: response.data, ...response }; - } else if (response && Array.isArray(response)) { - // Direct array format - setAlerts(response); - return { alerts: response }; - } else if (Array.isArray(response)) { - // Direct array format - setAlerts(response); - return { alerts: response }; - } else { - // Unknown format - return empty - setAlerts([]); - return { alerts: [] }; - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get forecast alerts'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const acknowledgeForecastAlert = useCallback(async ( - tenantId: string, - alertId: string - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const acknowledgedAlert = await forecastingService.acknowledgeForecastAlert(tenantId, alertId); - setAlerts(prev => (prev || []).map(alert => - alert.id === alertId ? acknowledgedAlert : alert - )); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to acknowledge alert'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const exportForecasts = useCallback(async ( - tenantId: string, - format: 'csv' | 'excel' | 'json', - params?: { - inventory_product_id?: string; // Primary way to filter by product - product_name?: string; // For backward compatibility - start_date?: string; - end_date?: string; - } - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const blob = await forecastingService.exportForecasts(tenantId, format, params); - - // Create download link - const url = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `forecasts.${format}`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(url); - } catch (error) { - const message = error instanceof Error ? error.message : 'Export failed'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - return { - forecasts, - batchForecasts, - quickForecasts, - alerts, - isLoading, - error, - createSingleForecast, - createBatchForecast, - getForecasts, - getBatchForecastStatus, - getQuickForecasts, - getForecastAlerts, - acknowledgeForecastAlert, - exportForecasts, - clearError: () => setError(null), - }; -}; diff --git a/fdev-ffrontend/src/api/hooks/useInventory.ts b/fdev-ffrontend/src/api/hooks/useInventory.ts deleted file mode 100644 index 32919f76..00000000 --- a/fdev-ffrontend/src/api/hooks/useInventory.ts +++ /dev/null @@ -1,536 +0,0 @@ -// frontend/src/api/hooks/useInventory.ts -/** - * Inventory Management React Hook - * Provides comprehensive state management for inventory operations - */ - -import { useState, useEffect, useCallback } from 'react'; -import toast from 'react-hot-toast'; - -import { - inventoryService, - InventoryItem, - StockLevel, - StockMovement, - InventorySearchParams, - CreateInventoryItemRequest, - UpdateInventoryItemRequest, - StockAdjustmentRequest, - PaginatedResponse, - InventoryDashboardData -} from '../services/inventory.service'; -import type { ProductInfo } from '../types'; - -import { useTenantId } from '../../hooks/useTenantId'; - -// ========== HOOK INTERFACES ========== - -interface UseInventoryReturn { - // State - items: InventoryItem[]; - stockLevels: Record; - movements: StockMovement[]; - dashboardData: InventoryDashboardData | null; - isLoading: boolean; - error: string | null; - pagination: { - page: number; - limit: number; - total: number; - totalPages: number; - }; - - // Actions - loadItems: (params?: InventorySearchParams) => Promise; - loadItem: (itemId: string) => Promise; - createItem: (data: CreateInventoryItemRequest) => Promise; - updateItem: (itemId: string, data: UpdateInventoryItemRequest) => Promise; - deleteItem: (itemId: string) => Promise; - - // Stock operations - loadStockLevels: () => Promise; - adjustStock: (itemId: string, adjustment: StockAdjustmentRequest) => Promise; - loadMovements: (params?: any) => Promise; - - - // Dashboard - loadDashboard: () => Promise; - - // Utility - searchItems: (query: string) => Promise; - refresh: () => Promise; - clearError: () => void; -} - -interface UseInventoryDashboardReturn { - dashboardData: InventoryDashboardData | null; - isLoading: boolean; - error: string | null; - refresh: () => Promise; -} - -interface UseInventoryItemReturn { - item: InventoryItem | null; - stockLevel: StockLevel | null; - recentMovements: StockMovement[]; - isLoading: boolean; - error: string | null; - updateItem: (data: UpdateInventoryItemRequest) => Promise; - adjustStock: (adjustment: StockAdjustmentRequest) => Promise; - refresh: () => Promise; -} - -// ========== MAIN INVENTORY HOOK ========== - -export const useInventory = (autoLoad = true): UseInventoryReturn => { - const { tenantId } = useTenantId(); - - // State - const [items, setItems] = useState([]); - const [stockLevels, setStockLevels] = useState>({}); - const [movements, setMovements] = useState([]); - const [dashboardData, setDashboardData] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [pagination, setPagination] = useState({ - page: 1, - limit: 20, - total: 0, - totalPages: 0 - }); - - // Clear error - const clearError = useCallback(() => setError(null), []); - - // Load inventory items - const loadItems = useCallback(async (params?: InventorySearchParams) => { - if (!tenantId) return; - - setIsLoading(true); - setError(null); - - try { - const response = await inventoryService.getInventoryItems(tenantId, params); - console.log('🔄 useInventory: Loaded items:', response.items); - setItems(response.items || []); // Ensure it's always an array - setPagination({ - page: response.page || 1, - limit: response.limit || 20, - total: response.total || 0, - totalPages: response.total_pages || 0 - }); - } catch (err: any) { - console.error('❌ useInventory: Error loading items:', err); - const errorMessage = err.response?.data?.detail || err.message || 'Error loading inventory items'; - - setError(errorMessage); - setItems([]); // Set empty array on error - - // Show appropriate error message - if (err.response?.status === 401) { - console.error('❌ useInventory: Authentication failed'); - } else if (err.response?.status === 403) { - toast.error('No tienes permisos para acceder a este inventario'); - } else { - toast.error(errorMessage); - } - } finally { - setIsLoading(false); - } - }, [tenantId]); - - // Load single item - const loadItem = useCallback(async (itemId: string): Promise => { - if (!tenantId) return null; - - try { - const item = await inventoryService.getInventoryItem(tenantId, itemId); - - // Update in local state if it exists - setItems(prev => prev.map(i => i.id === itemId ? item : i)); - - return item; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error loading item'; - setError(errorMessage); - return null; - } - }, [tenantId]); - - // Create item - const createItem = useCallback(async (data: CreateInventoryItemRequest): Promise => { - if (!tenantId) return null; - - setIsLoading(true); - - try { - const newItem = await inventoryService.createInventoryItem(tenantId, data); - setItems(prev => [newItem, ...prev]); - toast.success(`Created ${newItem.name} successfully`); - return newItem; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error creating item'; - setError(errorMessage); - toast.error(errorMessage); - return null; - } finally { - setIsLoading(false); - } - }, [tenantId]); - - // Update item - const updateItem = useCallback(async ( - itemId: string, - data: UpdateInventoryItemRequest - ): Promise => { - if (!tenantId) return null; - - try { - const updatedItem = await inventoryService.updateInventoryItem(tenantId, itemId, data); - setItems(prev => prev.map(i => i.id === itemId ? updatedItem : i)); - toast.success(`Updated ${updatedItem.name} successfully`); - return updatedItem; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error updating item'; - setError(errorMessage); - toast.error(errorMessage); - return null; - } - }, [tenantId]); - - // Delete item - const deleteItem = useCallback(async (itemId: string): Promise => { - if (!tenantId) return false; - - try { - await inventoryService.deleteInventoryItem(tenantId, itemId); - setItems(prev => prev.filter(i => i.id !== itemId)); - toast.success('Item deleted successfully'); - return true; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error deleting item'; - setError(errorMessage); - toast.error(errorMessage); - return false; - } - }, [tenantId]); - - // Load stock levels - const loadStockLevels = useCallback(async () => { - if (!tenantId) return; - - try { - const levels = await inventoryService.getAllStockLevels(tenantId); - const levelMap = levels.reduce((acc, level) => { - acc[level.item_id] = level; - return acc; - }, {} as Record); - setStockLevels(levelMap); - } catch (err: any) { - console.error('Error loading stock levels:', err); - // Don't show toast error for this as it's not critical for forecast page - } - }, [tenantId]); - - // Adjust stock - const adjustStock = useCallback(async ( - itemId: string, - adjustment: StockAdjustmentRequest - ): Promise => { - if (!tenantId) return null; - - try { - const movement = await inventoryService.adjustStock(tenantId, itemId, adjustment); - - // Update local movements - setMovements(prev => [movement, ...prev.slice(0, 49)]); // Keep last 50 - - // Reload stock level for this item - const updatedLevel = await inventoryService.getStockLevel(tenantId, itemId); - setStockLevels(prev => ({ ...prev, [itemId]: updatedLevel })); - - toast.success('Stock adjusted successfully'); - return movement; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error adjusting stock'; - setError(errorMessage); - toast.error(errorMessage); - return null; - } - }, [tenantId]); - - // Load movements - const loadMovements = useCallback(async (params?: any) => { - if (!tenantId) return; - - try { - const response = await inventoryService.getStockMovements(tenantId, params); - setMovements(response.items); - } catch (err: any) { - console.error('Error loading movements:', err); - } - }, [tenantId]); - - - // Load dashboard - const loadDashboard = useCallback(async () => { - if (!tenantId) return; - - try { - const data = await inventoryService.getDashboardData(tenantId); - setDashboardData(data); - } catch (err: any) { - console.error('Error loading dashboard:', err); - // Don't show toast error for this as it's not critical for forecast page - } - }, [tenantId]); - - // Search items - const searchItems = useCallback(async (query: string): Promise => { - if (!tenantId || !query.trim()) return []; - - try { - return await inventoryService.searchItems(tenantId, query); - } catch (err: any) { - console.error('Error searching items:', err); - return []; - } - }, [tenantId]); - - // Refresh all data - const refresh = useCallback(async () => { - await Promise.all([ - loadItems(), - loadStockLevels(), - loadDashboard() - ]); - }, [loadItems, loadStockLevels, loadDashboard]); - - // Auto-load on mount - useEffect(() => { - if (autoLoad && tenantId) { - refresh(); - } - }, [autoLoad, tenantId, refresh]); - - return { - // State - items, - stockLevels, - movements, - dashboardData, - isLoading, - error, - pagination, - - // Actions - loadItems, - loadItem, - createItem, - updateItem, - deleteItem, - - // Stock operations - loadStockLevels, - adjustStock, - loadMovements, - - // Dashboard - loadDashboard, - - // Utility - searchItems, - refresh, - clearError - }; -}; - -// ========== DASHBOARD HOOK ========== - -export const useInventoryDashboard = (): UseInventoryDashboardReturn => { - const { tenantId } = useTenantId(); - const [dashboardData, setDashboardData] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const refresh = useCallback(async () => { - if (!tenantId) return; - - setIsLoading(true); - setError(null); - - try { - const dashboard = await inventoryService.getDashboardData(tenantId); - - setDashboardData(dashboard); - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error loading dashboard'; - setError(errorMessage); - } finally { - setIsLoading(false); - } - }, [tenantId]); - - useEffect(() => { - if (tenantId) { - refresh(); - } - }, [tenantId, refresh]); - - return { - dashboardData, - isLoading, - error, - refresh - }; -}; - -// ========== SINGLE ITEM HOOK ========== - -export const useInventoryItem = (itemId: string): UseInventoryItemReturn => { - const { tenantId } = useTenantId(); - const [item, setItem] = useState(null); - const [stockLevel, setStockLevel] = useState(null); - const [recentMovements, setRecentMovements] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const refresh = useCallback(async () => { - if (!tenantId || !itemId) return; - - setIsLoading(true); - setError(null); - - try { - const [itemData, stockData, movementsData] = await Promise.all([ - inventoryService.getInventoryItem(tenantId, itemId), - inventoryService.getStockLevel(tenantId, itemId), - inventoryService.getStockMovements(tenantId, { item_id: itemId, limit: 10 }) - ]); - - setItem(itemData); - setStockLevel(stockData); - setRecentMovements(movementsData.items); - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error loading item'; - setError(errorMessage); - } finally { - setIsLoading(false); - } - }, [tenantId, itemId]); - - const updateItem = useCallback(async (data: UpdateInventoryItemRequest): Promise => { - if (!tenantId || !itemId) return false; - - try { - const updatedItem = await inventoryService.updateInventoryItem(tenantId, itemId, data); - setItem(updatedItem); - toast.success('Item updated successfully'); - return true; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error updating item'; - setError(errorMessage); - toast.error(errorMessage); - return false; - } - }, [tenantId, itemId]); - - const adjustStock = useCallback(async (adjustment: StockAdjustmentRequest): Promise => { - if (!tenantId || !itemId) return false; - - try { - const movement = await inventoryService.adjustStock(tenantId, itemId, adjustment); - - // Refresh data - const [updatedStock, updatedMovements] = await Promise.all([ - inventoryService.getStockLevel(tenantId, itemId), - inventoryService.getStockMovements(tenantId, { item_id: itemId, limit: 10 }) - ]); - - setStockLevel(updatedStock); - setRecentMovements(updatedMovements.items); - - toast.success('Stock adjusted successfully'); - return true; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error adjusting stock'; - setError(errorMessage); - toast.error(errorMessage); - return false; - } - }, [tenantId, itemId]); - - useEffect(() => { - if (tenantId && itemId) { - refresh(); - } - }, [tenantId, itemId, refresh]); - - return { - item, - stockLevel, - recentMovements, - isLoading, - error, - updateItem, - adjustStock, - refresh - }; -}; - -// ========== SIMPLE PRODUCTS HOOK FOR FORECASTING ========== - -export const useInventoryProducts = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - /** - * Get Products List for Forecasting - */ - const getProductsList = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const products = await inventoryService.getProductsList(tenantId); - - return products; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get products list'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - /** - * Get Product by ID - */ - const getProductById = useCallback(async (tenantId: string, productId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const product = await inventoryService.getProductById(tenantId, productId); - - return product; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get product'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - return { - // State - isLoading, - error, - - // Actions - getProductsList, - getProductById, - }; -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/hooks/useNotification.ts b/fdev-ffrontend/src/api/hooks/useNotification.ts deleted file mode 100644 index 43b059e7..00000000 --- a/fdev-ffrontend/src/api/hooks/useNotification.ts +++ /dev/null @@ -1,151 +0,0 @@ -// frontend/src/api/hooks/useNotification.ts -/** - * Notification Operations Hooks - */ - -import { useState, useCallback } from 'react'; -import { notificationService } from '../services'; -import type { - NotificationCreate, - NotificationResponse, - NotificationTemplate, - NotificationStats, - BulkNotificationRequest, -} from '../types'; - -export const useNotification = () => { - const [notifications, setNotifications] = useState([]); - const [templates, setTemplates] = useState([]); - const [stats, setStats] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const sendNotification = useCallback(async ( - tenantId: string, - notification: NotificationCreate - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const sentNotification = await notificationService.sendNotification(tenantId, notification); - setNotifications(prev => [sentNotification, ...prev]); - - return sentNotification; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to send notification'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const sendBulkNotifications = useCallback(async ( - tenantId: string, - request: BulkNotificationRequest - ): Promise => { - try { - setIsLoading(true); - setError(null); - - await notificationService.sendBulkNotifications(tenantId, request); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to send bulk notifications'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getNotifications = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const response = await notificationService.getNotifications(tenantId); - setNotifications(response.data); - - return response.data; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get notifications'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getTemplates = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const response = await notificationService.getTemplates(tenantId); - setTemplates(response.data); - - return response.data; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get templates'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const createTemplate = useCallback(async ( - tenantId: string, - template: Omit - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const newTemplate = await notificationService.createTemplate(tenantId, template); - setTemplates(prev => [newTemplate, ...prev]); - - return newTemplate; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to create template'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getNotificationStats = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const notificationStats = await notificationService.getNotificationStats(tenantId); - setStats(notificationStats); - - return notificationStats; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get notification stats'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - return { - notifications, - templates, - stats, - isLoading, - error, - sendNotification, - sendBulkNotifications, - getNotifications, - getTemplates, - createTemplate, - getNotificationStats, - clearError: () => setError(null), - }; -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/hooks/useOnboarding.ts b/fdev-ffrontend/src/api/hooks/useOnboarding.ts deleted file mode 100644 index 492a55bf..00000000 --- a/fdev-ffrontend/src/api/hooks/useOnboarding.ts +++ /dev/null @@ -1,194 +0,0 @@ -// frontend/src/api/hooks/useOnboarding.ts -/** - * Onboarding Hook - * React hook for managing user onboarding flow and progress - */ - -import { useState, useEffect } from 'react'; -import { onboardingService } from '../services/onboarding.service'; -import type { UserProgress, UpdateStepRequest } from '../services/onboarding.service'; - -export interface UseOnboardingReturn { - progress: UserProgress | null; - isLoading: boolean; - error: string | null; - currentStep: string | null; - nextStep: string | null; - completionPercentage: number; - isFullyComplete: boolean; - - // Actions - updateStep: (data: UpdateStepRequest) => Promise; - completeStep: (stepName: string, data?: Record) => Promise; - resetStep: (stepName: string) => Promise; - getNextStep: () => Promise; - completeOnboarding: () => Promise; - canAccessStep: (stepName: string) => Promise; - refreshProgress: () => Promise; -} - -export const useOnboarding = (): UseOnboardingReturn => { - const [progress, setProgress] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - // Derived state - const currentStep = progress?.current_step || null; - const nextStep = progress?.next_step || null; - const completionPercentage = progress?.completion_percentage || 0; - const isFullyComplete = progress?.fully_completed || false; - - // Load initial progress - const loadProgress = async () => { - setIsLoading(true); - setError(null); - - try { - const userProgress = await onboardingService.getUserProgress(); - setProgress(userProgress); - } catch (err) { - const message = err instanceof Error ? err.message : 'Failed to load onboarding progress'; - setError(message); - console.error('Onboarding progress load error:', err); - } finally { - setIsLoading(false); - } - }; - - // Update step - const updateStep = async (data: UpdateStepRequest) => { - setIsLoading(true); - setError(null); - - try { - const updatedProgress = await onboardingService.updateStep(data); - setProgress(updatedProgress); - } catch (err) { - const message = err instanceof Error ? err.message : 'Failed to update step'; - setError(message); - throw err; // Re-throw so calling component can handle it - } finally { - setIsLoading(false); - } - }; - - // Complete step with data - const completeStep = async (stepName: string, data?: Record) => { - await updateStep({ - step_name: stepName, - completed: true, - data - }); - }; - - // Reset step - const resetStep = async (stepName: string) => { - await updateStep({ - step_name: stepName, - completed: false - }); - }; - - // Get next step - const getNextStep = async (): Promise => { - try { - const result = await onboardingService.getNextStep(); - return result.step; - } catch (err) { - const message = err instanceof Error ? err.message : 'Failed to get next step'; - setError(message); - throw err; - } - }; - - // Complete entire onboarding - const completeOnboarding = async () => { - setIsLoading(true); - setError(null); - - try { - await onboardingService.completeOnboarding(); - await loadProgress(); // Refresh progress after completion - } catch (err) { - const message = err instanceof Error ? err.message : 'Failed to complete onboarding'; - setError(message); - throw err; - } finally { - setIsLoading(false); - } - }; - - // Check if user can access step - const canAccessStep = async (stepName: string): Promise => { - try { - const result = await onboardingService.canAccessStep(stepName); - return result.can_access; - } catch (err) { - console.error('Can access step check failed:', err); - return false; - } - }; - - // Refresh progress - const refreshProgress = async () => { - await loadProgress(); - }; - - // Load progress on mount - useEffect(() => { - loadProgress(); - }, []); - - return { - progress, - isLoading, - error, - currentStep, - nextStep, - completionPercentage, - isFullyComplete, - updateStep, - completeStep, - resetStep, - getNextStep, - completeOnboarding, - canAccessStep, - refreshProgress, - }; -}; - -// Helper hook for specific steps -export const useOnboardingStep = (stepName: string) => { - const onboarding = useOnboarding(); - - const stepStatus = onboarding.progress?.steps.find( - step => step.step_name === stepName - ); - - const isCompleted = stepStatus?.completed || false; - const stepData = stepStatus?.data || {}; - const completedAt = stepStatus?.completed_at; - - const completeThisStep = async (data?: Record) => { - await onboarding.completeStep(stepName, data); - }; - - const resetThisStep = async () => { - await onboarding.resetStep(stepName); - }; - - const canAccessThisStep = async (): Promise => { - return await onboarding.canAccessStep(stepName); - }; - - return { - ...onboarding, - stepName, - isCompleted, - stepData, - completedAt, - completeThisStep, - resetThisStep, - canAccessThisStep, - }; -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/hooks/usePOS.ts b/fdev-ffrontend/src/api/hooks/usePOS.ts deleted file mode 100644 index 044d968a..00000000 --- a/fdev-ffrontend/src/api/hooks/usePOS.ts +++ /dev/null @@ -1,337 +0,0 @@ -// frontend/src/api/hooks/usePOS.ts -/** - * React hooks for POS Integration functionality - */ - -import { useState, useEffect } from 'react'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { - posService, - POSConfiguration, - CreatePOSConfigurationRequest, - UpdatePOSConfigurationRequest, - POSTransaction, - POSSyncLog, - POSAnalytics, - SyncRequest -} from '../services/pos.service'; -import { useTenantId } from './useTenant'; - -// ============================================================================ -// CONFIGURATION HOOKS -// ============================================================================ - -export const usePOSConfigurations = (params?: { - pos_system?: string; - is_active?: boolean; - limit?: number; - offset?: number; -}) => { - const tenantId = useTenantId(); - - return useQuery({ - queryKey: ['pos-configurations', tenantId, params], - queryFn: () => posService.getConfigurations(tenantId, params), - enabled: !!tenantId, - staleTime: 5 * 60 * 1000, // 5 minutes - }); -}; - -export const usePOSConfiguration = (configId?: string) => { - const tenantId = useTenantId(); - - return useQuery({ - queryKey: ['pos-configuration', tenantId, configId], - queryFn: () => posService.getConfiguration(tenantId, configId!), - enabled: !!tenantId && !!configId, - staleTime: 5 * 60 * 1000, - }); -}; - -export const useCreatePOSConfiguration = () => { - const queryClient = useQueryClient(); - const tenantId = useTenantId(); - - return useMutation({ - mutationFn: (data: CreatePOSConfigurationRequest) => - posService.createConfiguration(tenantId, data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['pos-configurations', tenantId] }); - }, - }); -}; - -export const useUpdatePOSConfiguration = () => { - const queryClient = useQueryClient(); - const tenantId = useTenantId(); - - return useMutation({ - mutationFn: ({ configId, data }: { configId: string; data: UpdatePOSConfigurationRequest }) => - posService.updateConfiguration(tenantId, configId, data), - onSuccess: (_, { configId }) => { - queryClient.invalidateQueries({ queryKey: ['pos-configurations', tenantId] }); - queryClient.invalidateQueries({ queryKey: ['pos-configuration', tenantId, configId] }); - }, - }); -}; - -export const useDeletePOSConfiguration = () => { - const queryClient = useQueryClient(); - const tenantId = useTenantId(); - - return useMutation({ - mutationFn: (configId: string) => - posService.deleteConfiguration(tenantId, configId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['pos-configurations', tenantId] }); - }, - }); -}; - -export const useTestPOSConnection = () => { - const tenantId = useTenantId(); - - return useMutation({ - mutationFn: (configId: string) => - posService.testConnection(tenantId, configId), - }); -}; - -// ============================================================================ -// SYNCHRONIZATION HOOKS -// ============================================================================ - -export const useTriggerPOSSync = () => { - const queryClient = useQueryClient(); - const tenantId = useTenantId(); - - return useMutation({ - mutationFn: ({ configId, syncRequest }: { configId: string; syncRequest: SyncRequest }) => - posService.triggerSync(tenantId, configId, syncRequest), - onSuccess: (_, { configId }) => { - queryClient.invalidateQueries({ queryKey: ['pos-sync-status', tenantId, configId] }); - queryClient.invalidateQueries({ queryKey: ['pos-sync-logs', tenantId, configId] }); - }, - }); -}; - -export const usePOSSyncStatus = (configId?: string, pollingInterval?: number) => { - const tenantId = useTenantId(); - - return useQuery({ - queryKey: ['pos-sync-status', tenantId, configId], - queryFn: () => posService.getSyncStatus(tenantId, configId!), - enabled: !!tenantId && !!configId, - refetchInterval: pollingInterval || 30000, // Poll every 30 seconds by default - staleTime: 10 * 1000, // 10 seconds - }); -}; - -export const usePOSSyncLogs = (configId?: string, params?: { - limit?: number; - offset?: number; - status?: string; - sync_type?: string; - data_type?: string; -}) => { - const tenantId = useTenantId(); - - return useQuery({ - queryKey: ['pos-sync-logs', tenantId, configId, params], - queryFn: () => posService.getSyncLogs(tenantId, configId!, params), - enabled: !!tenantId && !!configId, - staleTime: 2 * 60 * 1000, // 2 minutes - }); -}; - -// ============================================================================ -// TRANSACTION HOOKS -// ============================================================================ - -export const usePOSTransactions = (params?: { - pos_system?: string; - start_date?: string; - end_date?: string; - status?: string; - is_synced?: boolean; - limit?: number; - offset?: number; -}) => { - const tenantId = useTenantId(); - - return useQuery({ - queryKey: ['pos-transactions', tenantId, params], - queryFn: () => posService.getTransactions(tenantId, params), - enabled: !!tenantId, - staleTime: 2 * 60 * 1000, // 2 minutes - }); -}; - -export const useSyncSingleTransaction = () => { - const queryClient = useQueryClient(); - const tenantId = useTenantId(); - - return useMutation({ - mutationFn: ({ transactionId, force }: { transactionId: string; force?: boolean }) => - posService.syncSingleTransaction(tenantId, transactionId, force), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['pos-transactions', tenantId] }); - }, - }); -}; - -export const useResyncFailedTransactions = () => { - const queryClient = useQueryClient(); - const tenantId = useTenantId(); - - return useMutation({ - mutationFn: (daysBack: number) => - posService.resyncFailedTransactions(tenantId, daysBack), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['pos-transactions', tenantId] }); - }, - }); -}; - -// ============================================================================ -// ANALYTICS HOOKS -// ============================================================================ - -export const usePOSAnalytics = (days: number = 30) => { - const tenantId = useTenantId(); - - return useQuery({ - queryKey: ['pos-analytics', tenantId, days], - queryFn: () => posService.getSyncAnalytics(tenantId, days), - enabled: !!tenantId, - staleTime: 10 * 60 * 1000, // 10 minutes - }); -}; - -// ============================================================================ -// SYSTEM INFO HOOKS -// ============================================================================ - -export const useSupportedPOSSystems = () => { - return useQuery({ - queryKey: ['supported-pos-systems'], - queryFn: () => posService.getSupportedSystems(), - staleTime: 60 * 60 * 1000, // 1 hour - }); -}; - -export const useWebhookStatus = (posSystem?: string) => { - return useQuery({ - queryKey: ['webhook-status', posSystem], - queryFn: () => posService.getWebhookStatus(posSystem!), - enabled: !!posSystem, - staleTime: 5 * 60 * 1000, // 5 minutes - }); -}; - -// ============================================================================ -// COMPOSITE HOOKS -// ============================================================================ - -export const usePOSDashboard = () => { - const tenantId = useTenantId(); - - // Get configurations - const { data: configurationsData, isLoading: configurationsLoading } = usePOSConfigurations(); - - // Get recent transactions - const { data: transactionsData, isLoading: transactionsLoading } = usePOSTransactions({ - limit: 10 - }); - - // Get analytics for last 7 days - const { data: analyticsData, isLoading: analyticsLoading } = usePOSAnalytics(7); - - const isLoading = configurationsLoading || transactionsLoading || analyticsLoading; - - return { - configurations: configurationsData?.configurations || [], - transactions: transactionsData?.transactions || [], - analytics: analyticsData, - isLoading, - summary: { - total_configurations: configurationsData?.total || 0, - active_configurations: configurationsData?.configurations?.filter(c => c.is_active).length || 0, - connected_configurations: configurationsData?.configurations?.filter(c => c.is_connected).length || 0, - total_transactions: transactionsData?.total || 0, - total_revenue: transactionsData?.summary?.total_amount || 0, - sync_health: analyticsData?.success_rate || 0, - } - }; -}; - -export const usePOSConfigurationManagement = () => { - const createMutation = useCreatePOSConfiguration(); - const updateMutation = useUpdatePOSConfiguration(); - const deleteMutation = useDeletePOSConfiguration(); - const testConnectionMutation = useTestPOSConnection(); - - const [selectedConfiguration, setSelectedConfiguration] = useState(null); - const [isFormOpen, setIsFormOpen] = useState(false); - - const handleCreate = async (data: CreatePOSConfigurationRequest) => { - await createMutation.mutateAsync(data); - setIsFormOpen(false); - }; - - const handleUpdate = async (configId: string, data: UpdatePOSConfigurationRequest) => { - await updateMutation.mutateAsync({ configId, data }); - setIsFormOpen(false); - setSelectedConfiguration(null); - }; - - const handleDelete = async (configId: string) => { - await deleteMutation.mutateAsync(configId); - }; - - const handleTestConnection = async (configId: string) => { - return await testConnectionMutation.mutateAsync(configId); - }; - - const openCreateForm = () => { - setSelectedConfiguration(null); - setIsFormOpen(true); - }; - - const openEditForm = (configuration: POSConfiguration) => { - setSelectedConfiguration(configuration); - setIsFormOpen(true); - }; - - const closeForm = () => { - setIsFormOpen(false); - setSelectedConfiguration(null); - }; - - return { - // State - selectedConfiguration, - isFormOpen, - - // Actions - handleCreate, - handleUpdate, - handleDelete, - handleTestConnection, - openCreateForm, - openEditForm, - closeForm, - - // Loading states - isCreating: createMutation.isPending, - isUpdating: updateMutation.isPending, - isDeleting: deleteMutation.isPending, - isTesting: testConnectionMutation.isPending, - - // Errors - createError: createMutation.error, - updateError: updateMutation.error, - deleteError: deleteMutation.error, - testError: testConnectionMutation.error, - }; -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/hooks/useProcurement.ts b/fdev-ffrontend/src/api/hooks/useProcurement.ts deleted file mode 100644 index 067fd9ec..00000000 --- a/fdev-ffrontend/src/api/hooks/useProcurement.ts +++ /dev/null @@ -1,294 +0,0 @@ -// ================================================================ -// frontend/src/api/hooks/useProcurement.ts -// ================================================================ -/** - * React hooks for procurement planning functionality - */ - -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { procurementService } from '../services/procurement.service'; -import type { - ProcurementPlan, - GeneratePlanRequest, - GeneratePlanResponse, - DashboardData, - ProcurementRequirement, - PaginatedProcurementPlans -} from '../types/procurement'; - -// ================================================================ -// QUERY KEYS -// ================================================================ - -export const procurementKeys = { - all: ['procurement'] as const, - plans: () => [...procurementKeys.all, 'plans'] as const, - plan: (id: string) => [...procurementKeys.plans(), id] as const, - currentPlan: () => [...procurementKeys.plans(), 'current'] as const, - planByDate: (date: string) => [...procurementKeys.plans(), 'date', date] as const, - plansList: (filters?: any) => [...procurementKeys.plans(), 'list', filters] as const, - requirements: () => [...procurementKeys.all, 'requirements'] as const, - planRequirements: (planId: string) => [...procurementKeys.requirements(), 'plan', planId] as const, - criticalRequirements: () => [...procurementKeys.requirements(), 'critical'] as const, - dashboard: () => [...procurementKeys.all, 'dashboard'] as const, -}; - -// ================================================================ -// PROCUREMENT PLAN HOOKS -// ================================================================ - -/** - * Hook to fetch the current day's procurement plan - */ -export function useCurrentProcurementPlan() { - return useQuery({ - queryKey: procurementKeys.currentPlan(), - queryFn: () => procurementService.getCurrentPlan(), - staleTime: 5 * 60 * 1000, // 5 minutes - refetchInterval: 10 * 60 * 1000, // Refetch every 10 minutes - }); -} - -/** - * Hook to fetch procurement plan by date - */ -export function useProcurementPlanByDate(date: string, enabled = true) { - return useQuery({ - queryKey: procurementKeys.planByDate(date), - queryFn: () => procurementService.getPlanByDate(date), - enabled: enabled && !!date, - staleTime: 30 * 60 * 1000, // 30 minutes for historical data - }); -} - -/** - * Hook to fetch procurement plan by ID - */ -export function useProcurementPlan(planId: string, enabled = true) { - return useQuery({ - queryKey: procurementKeys.plan(planId), - queryFn: () => procurementService.getPlanById(planId), - enabled: enabled && !!planId, - staleTime: 10 * 60 * 1000, // 10 minutes - }); -} - -/** - * Hook to fetch paginated list of procurement plans - */ -export function useProcurementPlans(params?: { - status?: string; - startDate?: string; - endDate?: string; - limit?: number; - offset?: number; -}) { - return useQuery({ - queryKey: procurementKeys.plansList(params), - queryFn: () => procurementService.listPlans(params), - staleTime: 5 * 60 * 1000, // 5 minutes - }); -} - -// ================================================================ -// REQUIREMENTS HOOKS -// ================================================================ - -/** - * Hook to fetch requirements for a specific plan - */ -export function usePlanRequirements( - planId: string, - filters?: { - status?: string; - priority?: string; - }, - enabled = true -) { - return useQuery({ - queryKey: procurementKeys.planRequirements(planId), - queryFn: () => procurementService.getPlanRequirements(planId, filters), - enabled: enabled && !!planId, - staleTime: 5 * 60 * 1000, // 5 minutes - }); -} - -/** - * Hook to fetch critical requirements across all plans - */ -export function useCriticalRequirements() { - return useQuery({ - queryKey: procurementKeys.criticalRequirements(), - queryFn: () => procurementService.getCriticalRequirements(), - staleTime: 2 * 60 * 1000, // 2 minutes for critical data - refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes - }); -} - -// ================================================================ -// DASHBOARD HOOKS -// ================================================================ - -/** - * Hook to fetch procurement dashboard data - */ -export function useProcurementDashboard() { - return useQuery({ - queryKey: procurementKeys.dashboard(), - queryFn: () => procurementService.getDashboardData(), - staleTime: 2 * 60 * 1000, // 2 minutes - refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes - }); -} - -// ================================================================ -// MUTATION HOOKS -// ================================================================ - -/** - * Hook to generate a new procurement plan - */ -export function useGenerateProcurementPlan() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (request: GeneratePlanRequest) => - procurementService.generatePlan(request), - onSuccess: (data: GeneratePlanResponse) => { - // Invalidate relevant queries - queryClient.invalidateQueries({ queryKey: procurementKeys.plans() }); - queryClient.invalidateQueries({ queryKey: procurementKeys.dashboard() }); - - // If plan was generated successfully, update the cache - if (data.success && data.plan) { - queryClient.setQueryData( - procurementKeys.plan(data.plan.id), - data.plan - ); - - // Update current plan cache if this is today's plan - const today = new Date().toISOString().split('T')[0]; - if (data.plan.plan_date === today) { - queryClient.setQueryData( - procurementKeys.currentPlan(), - data.plan - ); - } - } - }, - }); -} - -/** - * Hook to update procurement plan status - */ -export function useUpdatePlanStatus() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ planId, status }: { planId: string; status: string }) => - procurementService.updatePlanStatus(planId, status), - onSuccess: (updatedPlan: ProcurementPlan) => { - // Update the specific plan in cache - queryClient.setQueryData( - procurementKeys.plan(updatedPlan.id), - updatedPlan - ); - - // Update current plan if this is the current plan - const today = new Date().toISOString().split('T')[0]; - if (updatedPlan.plan_date === today) { - queryClient.setQueryData( - procurementKeys.currentPlan(), - updatedPlan - ); - } - - // Invalidate lists to ensure they're refreshed - queryClient.invalidateQueries({ queryKey: procurementKeys.plansList() }); - queryClient.invalidateQueries({ queryKey: procurementKeys.dashboard() }); - }, - }); -} - -/** - * Hook to trigger the daily scheduler manually - */ -export function useTriggerDailyScheduler() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: () => procurementService.triggerDailyScheduler(), - onSuccess: () => { - // Invalidate all procurement data - queryClient.invalidateQueries({ queryKey: procurementKeys.all }); - }, - }); -} - -// ================================================================ -// UTILITY HOOKS -// ================================================================ - -/** - * Hook to check procurement service health - */ -export function useProcurementHealth() { - return useQuery({ - queryKey: [...procurementKeys.all, 'health'], - queryFn: () => procurementService.healthCheck(), - staleTime: 60 * 1000, // 1 minute - refetchInterval: 5 * 60 * 1000, // Check every 5 minutes - }); -} - -// ================================================================ -// COMBINED HOOKS -// ================================================================ - -/** - * Combined hook for procurement plan dashboard - * Fetches current plan, dashboard data, and critical requirements - */ -export function useProcurementPlanDashboard() { - const currentPlan = useCurrentProcurementPlan(); - const dashboard = useProcurementDashboard(); - const criticalRequirements = useCriticalRequirements(); - const health = useProcurementHealth(); - - return { - currentPlan, - dashboard, - criticalRequirements, - health, - isLoading: currentPlan.isLoading || dashboard.isLoading, - error: currentPlan.error || dashboard.error || criticalRequirements.error, - refetchAll: () => { - currentPlan.refetch(); - dashboard.refetch(); - criticalRequirements.refetch(); - health.refetch(); - }, - }; -} - -/** - * Hook for managing procurement plan lifecycle - */ -export function useProcurementPlanActions() { - const generatePlan = useGenerateProcurementPlan(); - const updateStatus = useUpdatePlanStatus(); - const triggerScheduler = useTriggerDailyScheduler(); - - return { - generatePlan: generatePlan.mutate, - updateStatus: updateStatus.mutate, - triggerScheduler: triggerScheduler.mutate, - isGenerating: generatePlan.isPending, - isUpdating: updateStatus.isPending, - isTriggering: triggerScheduler.isPending, - generateError: generatePlan.error, - updateError: updateStatus.error, - triggerError: triggerScheduler.error, - }; -} \ No newline at end of file diff --git a/fdev-ffrontend/src/api/hooks/useRecipes.ts b/fdev-ffrontend/src/api/hooks/useRecipes.ts deleted file mode 100644 index 3e3f33b5..00000000 --- a/fdev-ffrontend/src/api/hooks/useRecipes.ts +++ /dev/null @@ -1,682 +0,0 @@ -// frontend/src/api/hooks/useRecipes.ts -/** - * React hooks for recipe and production management - */ - -import { useState, useEffect, useCallback, useMemo } from 'react'; -import { toast } from 'react-hot-toast'; -import { - RecipesService, - Recipe, - RecipeIngredient, - CreateRecipeRequest, - UpdateRecipeRequest, - RecipeSearchParams, - RecipeFeasibility, - RecipeStatistics, - ProductionBatch, - CreateProductionBatchRequest, - UpdateProductionBatchRequest, - ProductionBatchSearchParams, - ProductionStatistics -} from '../services/recipes.service'; -import { useTenant } from './useTenant'; -import { useAuth } from './useAuth'; - -const recipesService = new RecipesService(); - -// Recipe Management Hook -export interface UseRecipesReturn { - // Data - recipes: Recipe[]; - selectedRecipe: Recipe | null; - categories: string[]; - statistics: RecipeStatistics | null; - - // State - isLoading: boolean; - isCreating: boolean; - isUpdating: boolean; - isDeleting: boolean; - error: string | null; - - // Pagination - pagination: { - page: number; - limit: number; - total: number; - totalPages: number; - }; - - // Actions - loadRecipes: (params?: RecipeSearchParams) => Promise; - loadRecipe: (recipeId: string) => Promise; - createRecipe: (data: CreateRecipeRequest) => Promise; - updateRecipe: (recipeId: string, data: UpdateRecipeRequest) => Promise; - deleteRecipe: (recipeId: string) => Promise; - duplicateRecipe: (recipeId: string, newName: string) => Promise; - activateRecipe: (recipeId: string) => Promise; - checkFeasibility: (recipeId: string, batchMultiplier?: number) => Promise; - loadStatistics: () => Promise; - loadCategories: () => Promise; - clearError: () => void; - refresh: () => Promise; - setPage: (page: number) => void; -} - -export const useRecipes = (autoLoad: boolean = true): UseRecipesReturn => { - const { currentTenant } = useTenant(); - const { user } = useAuth(); - - // State - const [recipes, setRecipes] = useState([]); - const [selectedRecipe, setSelectedRecipe] = useState(null); - const [categories, setCategories] = useState([]); - const [statistics, setStatistics] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [isCreating, setIsCreating] = useState(false); - const [isUpdating, setIsUpdating] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const [error, setError] = useState(null); - const [currentParams, setCurrentParams] = useState({}); - const [pagination, setPagination] = useState({ - page: 1, - limit: 20, - total: 0, - totalPages: 0 - }); - - // Load recipes - const loadRecipes = useCallback(async (params: RecipeSearchParams = {}) => { - if (!currentTenant?.id) return; - - setIsLoading(true); - setError(null); - - try { - const searchParams = { - ...params, - limit: pagination.limit, - offset: (pagination.page - 1) * pagination.limit - }; - - const recipesData = await recipesService.getRecipes(currentTenant.id, searchParams); - setRecipes(recipesData); - setCurrentParams(params); - - // Calculate pagination (assuming we get total count somehow) - const total = recipesData.length; // This would need to be from a proper paginated response - setPagination(prev => ({ - ...prev, - total, - totalPages: Math.ceil(total / prev.limit) - })); - - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error loading recipes'; - setError(errorMessage); - toast.error(errorMessage); - } finally { - setIsLoading(false); - } - }, [currentTenant?.id, pagination.page, pagination.limit]); - - // Load single recipe - const loadRecipe = useCallback(async (recipeId: string) => { - if (!currentTenant?.id) return; - - setIsLoading(true); - setError(null); - - try { - const recipe = await recipesService.getRecipe(currentTenant.id, recipeId); - setSelectedRecipe(recipe); - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error loading recipe'; - setError(errorMessage); - toast.error(errorMessage); - } finally { - setIsLoading(false); - } - }, [currentTenant?.id]); - - // Create recipe - const createRecipe = useCallback(async (data: CreateRecipeRequest): Promise => { - if (!currentTenant?.id || !user?.id) return null; - - setIsCreating(true); - setError(null); - - try { - const newRecipe = await recipesService.createRecipe(currentTenant.id, user.id, data); - - // Add to local state - setRecipes(prev => [newRecipe, ...prev]); - - toast.success('Recipe created successfully'); - return newRecipe; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error creating recipe'; - setError(errorMessage); - toast.error(errorMessage); - return null; - } finally { - setIsCreating(false); - } - }, [currentTenant?.id, user?.id]); - - // Update recipe - const updateRecipe = useCallback(async (recipeId: string, data: UpdateRecipeRequest): Promise => { - if (!currentTenant?.id || !user?.id) return null; - - setIsUpdating(true); - setError(null); - - try { - const updatedRecipe = await recipesService.updateRecipe(currentTenant.id, user.id, recipeId, data); - - // Update local state - setRecipes(prev => prev.map(recipe => - recipe.id === recipeId ? updatedRecipe : recipe - )); - - if (selectedRecipe?.id === recipeId) { - setSelectedRecipe(updatedRecipe); - } - - toast.success('Recipe updated successfully'); - return updatedRecipe; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error updating recipe'; - setError(errorMessage); - toast.error(errorMessage); - return null; - } finally { - setIsUpdating(false); - } - }, [currentTenant?.id, user?.id, selectedRecipe?.id]); - - // Delete recipe - const deleteRecipe = useCallback(async (recipeId: string): Promise => { - if (!currentTenant?.id) return false; - - setIsDeleting(true); - setError(null); - - try { - await recipesService.deleteRecipe(currentTenant.id, recipeId); - - // Remove from local state - setRecipes(prev => prev.filter(recipe => recipe.id !== recipeId)); - - if (selectedRecipe?.id === recipeId) { - setSelectedRecipe(null); - } - - toast.success('Recipe deleted successfully'); - return true; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error deleting recipe'; - setError(errorMessage); - toast.error(errorMessage); - return false; - } finally { - setIsDeleting(false); - } - }, [currentTenant?.id, selectedRecipe?.id]); - - // Duplicate recipe - const duplicateRecipe = useCallback(async (recipeId: string, newName: string): Promise => { - if (!currentTenant?.id || !user?.id) return null; - - setIsCreating(true); - setError(null); - - try { - const duplicatedRecipe = await recipesService.duplicateRecipe(currentTenant.id, user.id, recipeId, newName); - - // Add to local state - setRecipes(prev => [duplicatedRecipe, ...prev]); - - toast.success('Recipe duplicated successfully'); - return duplicatedRecipe; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error duplicating recipe'; - setError(errorMessage); - toast.error(errorMessage); - return null; - } finally { - setIsCreating(false); - } - }, [currentTenant?.id, user?.id]); - - // Activate recipe - const activateRecipe = useCallback(async (recipeId: string): Promise => { - if (!currentTenant?.id || !user?.id) return null; - - setIsUpdating(true); - setError(null); - - try { - const activatedRecipe = await recipesService.activateRecipe(currentTenant.id, user.id, recipeId); - - // Update local state - setRecipes(prev => prev.map(recipe => - recipe.id === recipeId ? activatedRecipe : recipe - )); - - if (selectedRecipe?.id === recipeId) { - setSelectedRecipe(activatedRecipe); - } - - toast.success('Recipe activated successfully'); - return activatedRecipe; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error activating recipe'; - setError(errorMessage); - toast.error(errorMessage); - return null; - } finally { - setIsUpdating(false); - } - }, [currentTenant?.id, user?.id, selectedRecipe?.id]); - - // Check feasibility - const checkFeasibility = useCallback(async (recipeId: string, batchMultiplier: number = 1.0): Promise => { - if (!currentTenant?.id) return null; - - try { - const feasibility = await recipesService.checkRecipeFeasibility(currentTenant.id, recipeId, batchMultiplier); - return feasibility; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error checking recipe feasibility'; - setError(errorMessage); - toast.error(errorMessage); - return null; - } - }, [currentTenant?.id]); - - // Load statistics - const loadStatistics = useCallback(async () => { - if (!currentTenant?.id) return; - - try { - const stats = await recipesService.getRecipeStatistics(currentTenant.id); - setStatistics(stats); - } catch (err: any) { - console.error('Error loading recipe statistics:', err); - } - }, [currentTenant?.id]); - - // Load categories - const loadCategories = useCallback(async () => { - if (!currentTenant?.id) return; - - try { - const cats = await recipesService.getRecipeCategories(currentTenant.id); - setCategories(cats); - } catch (err: any) { - console.error('Error loading recipe categories:', err); - } - }, [currentTenant?.id]); - - // Clear error - const clearError = useCallback(() => { - setError(null); - }, []); - - // Refresh - const refresh = useCallback(async () => { - await Promise.all([ - loadRecipes(currentParams), - loadStatistics(), - loadCategories() - ]); - }, [loadRecipes, currentParams, loadStatistics, loadCategories]); - - // Set page - const setPage = useCallback((page: number) => { - setPagination(prev => ({ ...prev, page })); - }, []); - - // Auto-load on mount and dependencies change - useEffect(() => { - if (autoLoad && currentTenant?.id) { - refresh(); - } - }, [autoLoad, currentTenant?.id, pagination.page]); - - return { - // Data - recipes, - selectedRecipe, - categories, - statistics, - - // State - isLoading, - isCreating, - isUpdating, - isDeleting, - error, - pagination, - - // Actions - loadRecipes, - loadRecipe, - createRecipe, - updateRecipe, - deleteRecipe, - duplicateRecipe, - activateRecipe, - checkFeasibility, - loadStatistics, - loadCategories, - clearError, - refresh, - setPage - }; -}; - -// Production Management Hook -export interface UseProductionReturn { - // Data - batches: ProductionBatch[]; - selectedBatch: ProductionBatch | null; - activeBatches: ProductionBatch[]; - statistics: ProductionStatistics | null; - - // State - isLoading: boolean; - isCreating: boolean; - isUpdating: boolean; - isDeleting: boolean; - error: string | null; - - // Actions - loadBatches: (params?: ProductionBatchSearchParams) => Promise; - loadBatch: (batchId: string) => Promise; - loadActiveBatches: () => Promise; - createBatch: (data: CreateProductionBatchRequest) => Promise; - updateBatch: (batchId: string, data: UpdateProductionBatchRequest) => Promise; - deleteBatch: (batchId: string) => Promise; - startBatch: (batchId: string, data: any) => Promise; - completeBatch: (batchId: string, data: any) => Promise; - loadStatistics: (startDate?: string, endDate?: string) => Promise; - clearError: () => void; - refresh: () => Promise; -} - -export const useProduction = (autoLoad: boolean = true): UseProductionReturn => { - const { currentTenant } = useTenant(); - const { user } = useAuth(); - - // State - const [batches, setBatches] = useState([]); - const [selectedBatch, setSelectedBatch] = useState(null); - const [activeBatches, setActiveBatches] = useState([]); - const [statistics, setStatistics] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [isCreating, setIsCreating] = useState(false); - const [isUpdating, setIsUpdating] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const [error, setError] = useState(null); - - // Load batches - const loadBatches = useCallback(async (params: ProductionBatchSearchParams = {}) => { - if (!currentTenant?.id) return; - - setIsLoading(true); - setError(null); - - try { - const batchesData = await recipesService.getProductionBatches(currentTenant.id, params); - setBatches(batchesData); - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error loading production batches'; - setError(errorMessage); - toast.error(errorMessage); - } finally { - setIsLoading(false); - } - }, [currentTenant?.id]); - - // Load single batch - const loadBatch = useCallback(async (batchId: string) => { - if (!currentTenant?.id) return; - - setIsLoading(true); - setError(null); - - try { - const batch = await recipesService.getProductionBatch(currentTenant.id, batchId); - setSelectedBatch(batch); - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error loading production batch'; - setError(errorMessage); - toast.error(errorMessage); - } finally { - setIsLoading(false); - } - }, [currentTenant?.id]); - - // Load active batches - const loadActiveBatches = useCallback(async () => { - if (!currentTenant?.id) return; - - try { - const activeBatchesData = await recipesService.getActiveProductionBatches(currentTenant.id); - setActiveBatches(activeBatchesData); - } catch (err: any) { - console.error('Error loading active batches:', err); - } - }, [currentTenant?.id]); - - // Create batch - const createBatch = useCallback(async (data: CreateProductionBatchRequest): Promise => { - if (!currentTenant?.id || !user?.id) return null; - - setIsCreating(true); - setError(null); - - try { - const newBatch = await recipesService.createProductionBatch(currentTenant.id, user.id, data); - - // Add to local state - setBatches(prev => [newBatch, ...prev]); - - toast.success('Production batch created successfully'); - return newBatch; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error creating production batch'; - setError(errorMessage); - toast.error(errorMessage); - return null; - } finally { - setIsCreating(false); - } - }, [currentTenant?.id, user?.id]); - - // Update batch - const updateBatch = useCallback(async (batchId: string, data: UpdateProductionBatchRequest): Promise => { - if (!currentTenant?.id || !user?.id) return null; - - setIsUpdating(true); - setError(null); - - try { - const updatedBatch = await recipesService.updateProductionBatch(currentTenant.id, user.id, batchId, data); - - // Update local state - setBatches(prev => prev.map(batch => - batch.id === batchId ? updatedBatch : batch - )); - - if (selectedBatch?.id === batchId) { - setSelectedBatch(updatedBatch); - } - - toast.success('Production batch updated successfully'); - return updatedBatch; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error updating production batch'; - setError(errorMessage); - toast.error(errorMessage); - return null; - } finally { - setIsUpdating(false); - } - }, [currentTenant?.id, user?.id, selectedBatch?.id]); - - // Delete batch - const deleteBatch = useCallback(async (batchId: string): Promise => { - if (!currentTenant?.id) return false; - - setIsDeleting(true); - setError(null); - - try { - await recipesService.deleteProductionBatch(currentTenant.id, batchId); - - // Remove from local state - setBatches(prev => prev.filter(batch => batch.id !== batchId)); - - if (selectedBatch?.id === batchId) { - setSelectedBatch(null); - } - - toast.success('Production batch deleted successfully'); - return true; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error deleting production batch'; - setError(errorMessage); - toast.error(errorMessage); - return false; - } finally { - setIsDeleting(false); - } - }, [currentTenant?.id, selectedBatch?.id]); - - // Start batch - const startBatch = useCallback(async (batchId: string, data: any): Promise => { - if (!currentTenant?.id || !user?.id) return null; - - setIsUpdating(true); - setError(null); - - try { - const startedBatch = await recipesService.startProductionBatch(currentTenant.id, user.id, batchId, data); - - // Update local state - setBatches(prev => prev.map(batch => - batch.id === batchId ? startedBatch : batch - )); - - if (selectedBatch?.id === batchId) { - setSelectedBatch(startedBatch); - } - - toast.success('Production batch started successfully'); - return startedBatch; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error starting production batch'; - setError(errorMessage); - toast.error(errorMessage); - return null; - } finally { - setIsUpdating(false); - } - }, [currentTenant?.id, user?.id, selectedBatch?.id]); - - // Complete batch - const completeBatch = useCallback(async (batchId: string, data: any): Promise => { - if (!currentTenant?.id || !user?.id) return null; - - setIsUpdating(true); - setError(null); - - try { - const completedBatch = await recipesService.completeProductionBatch(currentTenant.id, user.id, batchId, data); - - // Update local state - setBatches(prev => prev.map(batch => - batch.id === batchId ? completedBatch : batch - )); - - if (selectedBatch?.id === batchId) { - setSelectedBatch(completedBatch); - } - - toast.success('Production batch completed successfully'); - return completedBatch; - } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Error completing production batch'; - setError(errorMessage); - toast.error(errorMessage); - return null; - } finally { - setIsUpdating(false); - } - }, [currentTenant?.id, user?.id, selectedBatch?.id]); - - // Load statistics - const loadStatistics = useCallback(async (startDate?: string, endDate?: string) => { - if (!currentTenant?.id) return; - - try { - const stats = await recipesService.getProductionStatistics(currentTenant.id, startDate, endDate); - setStatistics(stats); - } catch (err: any) { - console.error('Error loading production statistics:', err); - } - }, [currentTenant?.id]); - - // Clear error - const clearError = useCallback(() => { - setError(null); - }, []); - - // Refresh - const refresh = useCallback(async () => { - await Promise.all([ - loadBatches(), - loadActiveBatches(), - loadStatistics() - ]); - }, [loadBatches, loadActiveBatches, loadStatistics]); - - // Auto-load on mount - useEffect(() => { - if (autoLoad && currentTenant?.id) { - refresh(); - } - }, [autoLoad, currentTenant?.id]); - - return { - // Data - batches, - selectedBatch, - activeBatches, - statistics, - - // State - isLoading, - isCreating, - isUpdating, - isDeleting, - error, - - // Actions - loadBatches, - loadBatch, - loadActiveBatches, - createBatch, - updateBatch, - deleteBatch, - startBatch, - completeBatch, - loadStatistics, - clearError, - refresh - }; -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/hooks/useSales.ts b/fdev-ffrontend/src/api/hooks/useSales.ts deleted file mode 100644 index cbfa3fa3..00000000 --- a/fdev-ffrontend/src/api/hooks/useSales.ts +++ /dev/null @@ -1,200 +0,0 @@ -// frontend/src/api/hooks/useSales.ts -/** - * Sales Data Management Hooks - */ - -import { useState, useCallback } from 'react'; -import { salesService } from '../services/sales.service'; -import type { - SalesData, - SalesValidationResult, - SalesDataQuery, - SalesDataImport, - SalesImportResult, - DashboardStats, - ActivityItem, -} from '../types'; - -export const useSales = () => { - const [salesData, setSalesData] = useState([]); - const [dashboardStats, setDashboardStats] = useState(null); - const [recentActivity, setRecentActivity] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [uploadProgress, setUploadProgress] = useState(0); - - const uploadSalesHistory = useCallback(async ( - tenantId: string, - file: File, - additionalData?: Record - ): Promise => { - try { - setIsLoading(true); - setError(null); - setUploadProgress(0); - - const result = await salesService.uploadSalesHistory(tenantId, file, { - ...additionalData, - onProgress: (progress) => { - setUploadProgress(progress.percentage); - }, - }); - - return result; - } catch (error) { - const message = error instanceof Error ? error.message : 'Upload failed'; - setError(message); - throw error; - } finally { - setIsLoading(false); - setUploadProgress(0); - } - }, []); - - const validateSalesData = useCallback(async ( - tenantId: string, - file: File - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const result = await salesService.validateSalesData(tenantId, file); - return result; - } catch (error) { - const message = error instanceof Error ? error.message : 'Validation failed'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getSalesData = useCallback(async ( - tenantId: string, - query?: SalesDataQuery - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const response = await salesService.getSalesData(tenantId, query); - setSalesData(response.data); - - return response.data; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get sales data'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getDashboardStats = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const stats = await salesService.getDashboardStats(tenantId); - setDashboardStats(stats); - - return stats; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get dashboard stats'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getRecentActivity = useCallback(async (tenantId: string, limit?: number): Promise => { - try { - setIsLoading(true); - setError(null); - - const activity = await salesService.getRecentActivity(tenantId, limit); - setRecentActivity(activity); - - return activity; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get recent activity'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const exportSalesData = useCallback(async ( - tenantId: string, - format: 'csv' | 'excel' | 'json', - query?: SalesDataQuery - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const blob = await salesService.exportSalesData(tenantId, format, query); - - // Create download link - const url = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `sales-data.${format}`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(url); - } catch (error) { - const message = error instanceof Error ? error.message : 'Export failed'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - - /** - * Get Sales Analytics - */ - const getSalesAnalytics = useCallback(async ( - tenantId: string, - startDate?: string, - endDate?: string - ) => { - try { - setIsLoading(true); - setError(null); - - const analytics = await salesService.getSalesAnalytics(tenantId, startDate, endDate); - - return analytics; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get sales analytics'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - return { - salesData, - dashboardStats, - recentActivity, - isLoading, - error, - uploadProgress, - uploadSalesHistory, - validateSalesData, - getSalesData, - getDashboardStats, - getRecentActivity, - exportSalesData, - getSalesAnalytics, - clearError: () => setError(null), - }; -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/hooks/useSuppliers.ts b/fdev-ffrontend/src/api/hooks/useSuppliers.ts deleted file mode 100644 index 0d3cdefe..00000000 --- a/fdev-ffrontend/src/api/hooks/useSuppliers.ts +++ /dev/null @@ -1,101 +0,0 @@ -// Simplified useSuppliers hook for TypeScript compatibility -import { useState } from 'react'; -import { - SupplierSummary, - CreateSupplierRequest, - UpdateSupplierRequest, - SupplierSearchParams, - SupplierStatistics, - PurchaseOrder, - CreatePurchaseOrderRequest, - PurchaseOrderSearchParams, - PurchaseOrderStatistics, - Delivery, - DeliverySearchParams, - DeliveryPerformanceStats -} from '../services/suppliers.service'; - -export const useSuppliers = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - // Simple stub implementations - const getSuppliers = async (params?: SupplierSearchParams) => { - setIsLoading(true); - try { - // Mock data for now - return []; - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); - throw err; - } finally { - setIsLoading(false); - } - }; - - const createSupplier = async (data: CreateSupplierRequest) => { - setIsLoading(true); - try { - // Mock implementation - return { id: '1', ...data } as any; - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); - throw err; - } finally { - setIsLoading(false); - } - }; - - const updateSupplier = async (id: string, data: UpdateSupplierRequest) => { - setIsLoading(true); - try { - // Mock implementation - return { id, ...data } as any; - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); - throw err; - } finally { - setIsLoading(false); - } - }; - - // Return all the expected properties/methods - return { - suppliers: [], - isLoading, - error, - getSuppliers, - createSupplier, - updateSupplier, - deleteSupplier: async () => {}, - getSupplierStatistics: async () => ({} as SupplierStatistics), - getActiveSuppliers: async () => [] as SupplierSummary[], - getTopSuppliers: async () => [] as SupplierSummary[], - getSuppliersNeedingReview: async () => [] as SupplierSummary[], - approveSupplier: async () => {}, - // Purchase orders - getPurchaseOrders: async () => [] as PurchaseOrder[], - createPurchaseOrder: async () => ({} as PurchaseOrder), - updatePurchaseOrderStatus: async () => ({} as PurchaseOrder), - // Deliveries - getDeliveries: async () => [] as Delivery[], - getTodaysDeliveries: async () => [] as Delivery[], - getDeliveryPerformanceStats: async () => ({} as DeliveryPerformanceStats), - }; -}; - -// Re-export types -export type { - SupplierSummary, - CreateSupplierRequest, - UpdateSupplierRequest, - SupplierSearchParams, - SupplierStatistics, - PurchaseOrder, - CreatePurchaseOrderRequest, - PurchaseOrderSearchParams, - PurchaseOrderStatistics, - Delivery, - DeliverySearchParams, - DeliveryPerformanceStats -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/hooks/useTenant.ts b/fdev-ffrontend/src/api/hooks/useTenant.ts deleted file mode 100644 index 4cbc87ca..00000000 --- a/fdev-ffrontend/src/api/hooks/useTenant.ts +++ /dev/null @@ -1,209 +0,0 @@ -// frontend/src/api/hooks/useTenant.ts -/** - * Tenant Management Hooks - */ - -import { useState, useCallback } from 'react'; -import { tenantService } from '../services'; -import type { - TenantInfo, - TenantCreate, - TenantUpdate, - TenantMember, - InviteUser, - TenantStats, -} from '../types'; - -export const useTenant = () => { - const [tenants, setTenants] = useState([]); - const [currentTenant, setCurrentTenant] = useState(null); - const [members, setMembers] = useState([]); - const [stats, setStats] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const createTenant = useCallback(async (data: TenantCreate): Promise => { - try { - setIsLoading(true); - setError(null); - - const tenant = await tenantService.createTenant(data); - setTenants(prev => [...prev, tenant]); - setCurrentTenant(tenant); - - return tenant; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to create tenant'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getTenant = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const tenant = await tenantService.getTenant(tenantId); - setCurrentTenant(tenant); - - return tenant; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get tenant'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const updateTenant = useCallback(async (tenantId: string, data: TenantUpdate): Promise => { - try { - setIsLoading(true); - setError(null); - - const updatedTenant = await tenantService.updateTenant(tenantId, data); - setCurrentTenant(updatedTenant); - setTenants(prev => prev.map(t => t.id === tenantId ? updatedTenant : t)); - - return updatedTenant; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to update tenant'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getUserTenants = useCallback(async (): Promise => { - try { - setIsLoading(true); - setError(null); - - const userTenants = await tenantService.getUserTenants(); - setTenants(userTenants); - - return userTenants; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get user tenants'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getTenantMembers = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const response = await tenantService.getTenantMembers(tenantId); - setMembers(response.data); - - return response.data; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get tenant members'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const inviteUser = useCallback(async (tenantId: string, invitation: InviteUser): Promise => { - try { - setIsLoading(true); - setError(null); - - await tenantService.inviteUser(tenantId, invitation); - - // Refresh members list - await getTenantMembers(tenantId); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to invite user'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, [getTenantMembers]); - - const removeMember = useCallback(async (tenantId: string, userId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - await tenantService.removeMember(tenantId, userId); - setMembers(prev => prev.filter(m => m.user_id !== userId)); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to remove member'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const updateMemberRole = useCallback(async (tenantId: string, userId: string, role: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const updatedMember = await tenantService.updateMemberRole(tenantId, userId, role); - setMembers(prev => prev.map(m => m.user_id === userId ? updatedMember : m)); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to update member role'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getTenantStats = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const tenantStats = await tenantService.getTenantStats(tenantId); - setStats(tenantStats); - - return tenantStats; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get tenant stats'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - return { - tenants, - currentTenant, - members, - stats, - isLoading, - error, - createTenant, - getTenant, - updateTenant, - getUserTenants, - getTenantMembers, - inviteUser, - removeMember, - updateMemberRole, - getTenantStats, - clearError: () => setError(null), - }; -}; - -// Hook to get current tenant ID from context or state -export const useTenantId = () => { - const { currentTenant } = useTenant(); - return currentTenant?.id || null; -}; diff --git a/fdev-ffrontend/src/api/hooks/useTraining.ts b/fdev-ffrontend/src/api/hooks/useTraining.ts deleted file mode 100644 index 49efbd33..00000000 --- a/fdev-ffrontend/src/api/hooks/useTraining.ts +++ /dev/null @@ -1,265 +0,0 @@ -// frontend/src/api/hooks/useTraining.ts -/** - * Training Operations Hooks - */ - -import { useState, useCallback, useEffect } from 'react'; -import { trainingService } from '../services'; -import type { - TrainingJobRequest, - TrainingJobResponse, - ModelInfo, - ModelTrainingStats, - SingleProductTrainingRequest, -} from '../types'; - -interface UseTrainingOptions { - disablePolling?: boolean; // New option to disable HTTP status polling -} - -export const useTraining = (options: UseTrainingOptions = {}) => { - - const { disablePolling = false } = options; - - // Debug logging for option changes - console.log('🔧 useTraining initialized with options:', { disablePolling, options }); - const [jobs, setJobs] = useState([]); - const [currentJob, setCurrentJob] = useState(null); - const [models, setModels] = useState([]); - const [stats, setStats] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const startTrainingJob = useCallback(async ( - tenantId: string, - request: TrainingJobRequest - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const job = await trainingService.startTrainingJob(tenantId, request); - setCurrentJob(job); - setJobs(prev => [job, ...prev]); - - return job; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to start training job'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const startSingleProductTraining = useCallback(async ( - tenantId: string, - request: SingleProductTrainingRequest - ): Promise => { - try { - setIsLoading(true); - setError(null); - - const job = await trainingService.startSingleProductTraining(tenantId, request); - setCurrentJob(job); - setJobs(prev => [job, ...prev]); - - return job; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to start product training'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getTrainingJobStatus = useCallback(async ( - tenantId: string, - jobId: string - ): Promise => { - try { - const job = await trainingService.getTrainingJobStatus(tenantId, jobId); - - // Update job in state - setJobs(prev => prev.map(j => j.job_id === jobId ? job : j)); - if (currentJob?.job_id === jobId) { - setCurrentJob(job); - } - - return job; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get job status'; - setError(message); - throw error; - } - }, [currentJob]); - - const cancelTrainingJob = useCallback(async ( - tenantId: string, - jobId: string - ): Promise => { - try { - setIsLoading(true); - setError(null); - - await trainingService.cancelTrainingJob(tenantId, jobId); - - // Update job status in state - setJobs(prev => prev.map(j => - j.job_id === jobId ? { ...j, status: 'cancelled' } : j - )); - if (currentJob?.job_id === jobId) { - setCurrentJob({ ...currentJob, status: 'cancelled' }); - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to cancel job'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, [currentJob]); - - const getTrainingJobs = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const response = await trainingService.getTrainingJobs(tenantId); - setJobs(response.data); - - return response.data; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get training jobs'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getModels = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const response = await trainingService.getModels(tenantId); - setModels(response.data); - - return response.data; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get models'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const validateTrainingData = useCallback(async (tenantId: string): Promise<{ - is_valid: boolean; - message: string; - details?: any; - }> => { - try { - setIsLoading(true); - setError(null); - - const result = await trainingService.validateTrainingData(tenantId); - return result; - } catch (error) { - const message = error instanceof Error ? error.message : 'Data validation failed'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - const getTrainingStats = useCallback(async (tenantId: string): Promise => { - try { - setIsLoading(true); - setError(null); - - const trainingStats = await trainingService.getTrainingStats(tenantId); - setStats(trainingStats); - - return trainingStats; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get training stats'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - - useEffect(() => { - // Always check disablePolling first and log for debugging - console.log('🔍 useTraining polling check:', { - disablePolling, - jobsCount: jobs.length, - runningJobs: jobs.filter(job => job.status === 'running' || job.status === 'pending').length - }); - - // STRICT CHECK: Skip polling if disabled - NO EXCEPTIONS - if (disablePolling === true) { - console.log('🚫 HTTP status polling STRICTLY DISABLED - using WebSocket instead'); - console.log('🚫 Effect triggered but polling prevented by disablePolling flag'); - return; // Early return - no cleanup needed, no interval creation - } - - const runningJobs = jobs.filter(job => job.status === 'running' || job.status === 'pending'); - - if (runningJobs.length === 0) { - console.log('⏸️ No running jobs - skipping polling setup'); - return; - } - - console.log('🔄 Starting HTTP status polling for', runningJobs.length, 'jobs'); - - const interval = setInterval(async () => { - // Double-check disablePolling inside interval to prevent race conditions - if (disablePolling) { - console.log('🚫 Polling disabled during interval - clearing'); - clearInterval(interval); - return; - } - - for (const job of runningJobs) { - try { - const tenantId = job.tenant_id; - console.log('📡 HTTP polling job status:', job.job_id); - await getTrainingJobStatus(tenantId, job.job_id); - } catch (error) { - console.error('Failed to refresh job status:', error); - } - } - }, 5000); // Refresh every 5 seconds - - return () => { - console.log('🛑 Stopping HTTP status polling (cleanup)'); - clearInterval(interval); - }; - }, [jobs, getTrainingJobStatus, disablePolling]); - - - return { - jobs, - currentJob, - models, - stats, - isLoading, - error, - startTrainingJob, - startSingleProductTraining, - getTrainingJobStatus, - cancelTrainingJob, - getTrainingJobs, - getModels, - validateTrainingData, - getTrainingStats, - clearError: () => setError(null), - }; -}; diff --git a/fdev-ffrontend/src/api/index.ts b/fdev-ffrontend/src/api/index.ts deleted file mode 100644 index 701c823f..00000000 --- a/fdev-ffrontend/src/api/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -// frontend/src/api/index.ts -/** - * Main API Export - * Central entry point for all API functionality - */ - -// Export main API client first -export { apiClient } from './client'; - -// Export all services -export { - authService, - tenantService, - salesService, - externalService, - trainingService, - forecastingService, - notificationService, - inventoryService, - api -} from './services'; - -// Export all hooks -export { - useAuth, - useAuthHeaders, - useTenant, - useSales, - useExternal, - useTraining, - useForecast, - useNotification, - useApiHooks, - useOnboarding, - useInventory, - useInventoryProducts -} from './hooks'; - -// Export WebSocket functionality -export { - WebSocketManager, - useWebSocket, - useTrainingWebSocket, - useForecastWebSocket, -} from './websocket'; - -// Export WebSocket types -export type { - WebSocketConfig, - WebSocketMessage, - WebSocketHandlers, - WebSocketStatus, - WebSocketMetrics, -} from './websocket'; - -// Export types -export * from './types'; - -// Export configuration -export { apiConfig, serviceEndpoints, featureFlags } from './client/config'; - -// Setup interceptors on import (move to end to avoid circular deps) -import { setupInterceptors } from './client/interceptors'; -setupInterceptors(); \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/auth.service.ts b/fdev-ffrontend/src/api/services/auth.service.ts deleted file mode 100644 index dd2a8c2d..00000000 --- a/fdev-ffrontend/src/api/services/auth.service.ts +++ /dev/null @@ -1,107 +0,0 @@ -// frontend/src/api/services/auth.service.ts -/** - * Authentication Service - * Handles all authentication-related API calls - */ - -import { apiClient } from '../client'; -import { serviceEndpoints } from '../client/config'; -import type { - LoginRequest, - LoginResponse, - RegisterRequest, - UserResponse, - PasswordResetRequest, - PasswordResetResponse, - PasswordResetConfirmRequest, - TokenVerification, - LogoutResponse, -} from '../types'; - -export class AuthService { - private baseEndpoint = serviceEndpoints.auth; - - /** - * User Registration - */ - async register(data: RegisterRequest): Promise { - return apiClient.post(`${this.baseEndpoint}/register`, data); - } - - /** - * User Login - */ - async login(credentials: LoginRequest): Promise { - return apiClient.post(`${this.baseEndpoint}/login`, credentials); - } - - /** - * User Logout - */ - async logout(): Promise { - return apiClient.post(`${this.baseEndpoint}/logout`); - } - - /** - * Get Current User Profile - */ - async getCurrentUser(): Promise { - return apiClient.get(`/users/me`); - } - - /** - * Update User Profile - */ - async updateProfile(data: Partial): Promise { - return apiClient.put(`/users/me`, data); - } - - /** - * Verify Token - */ - async verifyToken(token: string): Promise { - return apiClient.post(`${this.baseEndpoint}/verify-token`, { token }); - } - - /** - * Refresh Access Token - */ - async refreshToken(refreshToken: string): Promise { - return apiClient.post(`${this.baseEndpoint}/refresh`, { - refresh_token: refreshToken, - }); - } - - /** - * Request Password Reset - */ - async requestPasswordReset(data: PasswordResetRequest): Promise { - return apiClient.post(`${this.baseEndpoint}/password-reset`, data); - } - - /** - * Confirm Password Reset - */ - async confirmPasswordReset(data: PasswordResetConfirmRequest): Promise<{ message: string }> { - return apiClient.post(`${this.baseEndpoint}/password-reset/confirm`, data); - } - - /** - * Change Password (for authenticated users) - */ - async changePassword(currentPassword: string, newPassword: string): Promise<{ message: string }> { - return apiClient.post(`/users/me/change-password`, { - current_password: currentPassword, - new_password: newPassword, - }); - } - - /** - * Delete User Account - */ - async deleteAccount(): Promise<{ message: string }> { - return apiClient.delete(`/users/me`); - } -} - -export const authService = new AuthService(); \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/external.service.ts b/fdev-ffrontend/src/api/services/external.service.ts deleted file mode 100644 index c6756d9a..00000000 --- a/fdev-ffrontend/src/api/services/external.service.ts +++ /dev/null @@ -1,296 +0,0 @@ -// frontend/src/api/services/external.service.ts -/** - * External Data Service - * Handles weather and traffic data operations for the external microservice - */ - -import { apiClient } from '../client'; -import { RequestTimeouts } from '../client/config'; - -// Align with backend WeatherDataResponse schema -export interface WeatherData { - date: string; - temperature?: number; - precipitation?: number; - humidity?: number; - wind_speed?: number; - pressure?: number; - description?: string; - source: string; -} - -// Align with backend TrafficDataResponse schema -export interface TrafficData { - date: string; - traffic_volume?: number; - pedestrian_count?: number; - congestion_level?: string; - average_speed?: number; - source: string; -} - -export interface WeatherForecast { - date: string; - temperature_min: number; - temperature_max: number; - temperature_avg: number; - precipitation: number; - description: string; - humidity?: number; - wind_speed?: number; -} - -export interface HourlyForecast { - forecast_datetime: string; - generated_at: string; - temperature: number; - precipitation: number; - humidity: number; - wind_speed: number; - description: string; - source: string; - hour: number; -} - -export class ExternalService { - /** - * Get Current Weather Data - */ - async getCurrentWeather( - tenantId: string, - lat: number, - lon: number - ): Promise { - try { - // ✅ FIX 1: Correct endpoint path with tenant ID - const endpoint = `/tenants/${tenantId}/weather/current`; - - // ✅ FIX 2: Correct parameter names (latitude/longitude, not lat/lon) - const response = await apiClient.get(endpoint, { - params: { - latitude: lat, // Backend expects 'latitude' - longitude: lon // Backend expects 'longitude' - } - }); - - console.log('Weather API response:', response); - - // Return backend response directly (matches WeatherData interface) - return response; - - } catch (error) { - console.error('Failed to fetch weather from AEMET API via backend:', error); - throw new Error(`Weather data unavailable: ${error instanceof Error ? error.message : 'AEMET API connection failed'}`); - } - } - - /** - * Get Weather Forecast - */ - async getWeatherForecast( - tenantId: string, - lat: number, - lon: number, - days: number = 7 - ): Promise { - try { - // Fix: Use POST with JSON body as expected by backend - const response = await apiClient.post(`/tenants/${tenantId}/weather/forecast`, { - latitude: lat, - longitude: lon, - days: days - }); - - // Handle response format - if (Array.isArray(response)) { - return response; - } else if (response && response.forecasts) { - return response.forecasts; - } else { - console.warn('Unexpected weather forecast response format:', response); - return []; - } - } catch (error) { - console.error('Failed to fetch weather forecast from AEMET API:', error); - throw new Error(`Weather forecast unavailable: ${error instanceof Error ? error.message : 'AEMET API connection failed'}`); - } - } - - /** - * Get Hourly Weather Forecast (NEW) - */ - async getHourlyWeatherForecast( - tenantId: string, - lat: number, - lon: number, - hours: number = 48 - ): Promise { - try { - console.log(`🕒 Fetching hourly weather forecast from AEMET API for tenant ${tenantId}`, { - latitude: lat, - longitude: lon, - hours: hours - }); - - const response = await apiClient.post(`/tenants/${tenantId}/weather/hourly-forecast`, { - latitude: lat, - longitude: lon, - hours: hours - }); - - // Handle response format - if (Array.isArray(response)) { - return response; - } else if (response && response.data) { - return response.data; - } else { - console.warn('Unexpected hourly forecast response format:', response); - return []; - } - } catch (error) { - console.error('Failed to fetch hourly forecast from AEMET API:', error); - throw new Error(`Hourly forecast unavailable: ${error instanceof Error ? error.message : 'AEMET API connection failed'}`); - } - } - - /** - * Get Historical Weather Data - */ - async getHistoricalWeather( - tenantId: string, - lat: number, - lon: number, - startDate: string, - endDate: string - ): Promise { - try { - // Fix: Use POST with JSON body as expected by backend - const response = await apiClient.post(`/tenants/${tenantId}/weather/historical`, { - latitude: lat, - longitude: lon, - start_date: startDate, - end_date: endDate - }); - - // Return backend response directly (matches WeatherData interface) - return Array.isArray(response) ? response : response.data || []; - } catch (error) { - console.error('Failed to fetch historical weather from AEMET API:', error); - throw new Error(`Historical weather data unavailable: ${error instanceof Error ? error.message : 'AEMET API connection failed'}`); - } - } - - /** - * Get Current Traffic Data - */ - async getCurrentTraffic( - tenantId: string, - lat: number, - lon: number - ): Promise { - try { - const response = await apiClient.get(`/tenants/${tenantId}/traffic/current`, { - params: { - latitude: lat, - longitude: lon - } - }); - - // Return backend response directly (matches TrafficData interface) - return response; - } catch (error) { - console.error('Failed to fetch traffic data from external API:', error); - throw new Error(`Traffic data unavailable: ${error instanceof Error ? error.message : 'External API connection failed'}`); - } - } - - /** - * Get Traffic Forecast - */ - async getTrafficForecast( - tenantId: string, - lat: number, - lon: number, - hours: number = 24 - ): Promise { - try { - // Fix: Use POST with JSON body as expected by backend - const response = await apiClient.post(`/tenants/${tenantId}/traffic/forecast`, { - latitude: lat, - longitude: lon, - hours: hours - }); - - // Return backend response directly (matches TrafficData interface) - return Array.isArray(response) ? response : response.data || []; - } catch (error) { - console.error('Failed to fetch traffic forecast from external API:', error); - throw new Error(`Traffic forecast unavailable: ${error instanceof Error ? error.message : 'External API connection failed'}`); - } - } - - /** - * Get Historical Traffic Data - */ - async getHistoricalTraffic( - tenantId: string, - lat: number, - lon: number, - startDate: string, - endDate: string - ): Promise { - try { - // Fix: Use POST with JSON body as expected by backend - const response = await apiClient.post(`/tenants/${tenantId}/traffic/historical`, { - latitude: lat, - longitude: lon, - start_date: startDate, - end_date: endDate - }); - - // Return backend response directly (matches TrafficData interface) - return Array.isArray(response) ? response : response.data || []; - } catch (error) { - console.error('Failed to fetch historical traffic from external API:', error); - throw new Error(`Historical traffic data unavailable: ${error instanceof Error ? error.message : 'External API connection failed'}`); - } - } - - /** - * Test External Service Connectivity - */ - async testConnectivity(tenantId: string): Promise<{ - weather: boolean; - traffic: boolean; - overall: boolean; - }> { - const results = { - weather: false, - traffic: false, - overall: false - }; - - try { - // Test weather service (AEMET API) - await this.getCurrentWeather(tenantId, 40.4168, -3.7038); // Madrid coordinates - results.weather = true; - } catch (error) { - console.warn('AEMET weather service connectivity test failed:', error); - results.weather = false; - } - - try { - // Test traffic service - await this.getCurrentTraffic(tenantId, 40.4168, -3.7038); // Madrid coordinates - results.traffic = true; - } catch (error) { - console.warn('Traffic service connectivity test failed:', error); - results.traffic = false; - } - - results.overall = results.weather && results.traffic; - return results; - } -} - -export const externalService = new ExternalService(); \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/forecasting.service.ts b/fdev-ffrontend/src/api/services/forecasting.service.ts deleted file mode 100644 index 83ef95f1..00000000 --- a/fdev-ffrontend/src/api/services/forecasting.service.ts +++ /dev/null @@ -1,301 +0,0 @@ -// frontend/src/api/services/forecasting.service.ts -/** - * Forecasting Service - * Handles forecast operations and predictions - */ - -import { apiClient } from '../client'; -import { RequestTimeouts } from '../client/config'; -import type { - SingleForecastRequest, - BatchForecastRequest, - ForecastResponse, - BatchForecastResponse, - ForecastAlert, - QuickForecast, - PaginatedResponse, - BaseQueryParams, -} from '../types'; - -export class ForecastingService { - /** - * Create Single Product Forecast - */ - async createSingleForecast( - tenantId: string, - request: SingleForecastRequest - ): Promise { - console.log('🔮 Creating single forecast:', { tenantId, request }); - - try { - // Backend returns single ForecastResponse object - const response = await apiClient.post( - `/tenants/${tenantId}/forecasts/single`, - request, - { - timeout: RequestTimeouts.MEDIUM, - } - ); - - console.log('🔮 Forecast API Response:', response); - console.log('- Type:', typeof response); - console.log('- Is Array:', Array.isArray(response)); - - // ✅ FIX: Convert single response to array - if (response && typeof response === 'object' && !Array.isArray(response)) { - // Single forecast response - wrap in array - const forecastArray = [response as ForecastResponse]; - console.log('✅ Converted single forecast to array:', forecastArray); - return forecastArray; - } else if (Array.isArray(response)) { - // Already an array (unexpected but handle gracefully) - console.log('✅ Response is already an array:', response); - return response; - } else { - console.error('❌ Unexpected response format:', response); - throw new Error('Invalid forecast response format'); - } - - } catch (error) { - console.error('❌ Forecast API Error:', error); - throw error; - } - } - - /** - * Create Batch Forecast - */ - async createBatchForecast( - tenantId: string, - request: BatchForecastRequest - ): Promise { - return apiClient.post( - `/tenants/${tenantId}/forecasts/batch`, - request, - { - timeout: RequestTimeouts.LONG, - } - ); - } - - /** - * Get Forecast by ID - */ - async getForecast(tenantId: string, forecastId: string): Promise { - return apiClient.get(`/tenants/${tenantId}/forecasts/${forecastId}`); - } - - /** - * Get Forecasts - */ - async getForecasts( - tenantId: string, - params?: BaseQueryParams & { - inventory_product_id?: string; // Primary way to filter by product - product_name?: string; // For backward compatibility - will need inventory service lookup - start_date?: string; - end_date?: string; - model_id?: string; - } - ): Promise> { - return apiClient.get(`/tenants/${tenantId}/forecasts`, { params }); - } - - /** - * Get Batch Forecast Status - */ - async getBatchForecastStatus( - tenantId: string, - batchId: string - ): Promise { - return apiClient.get(`/tenants/${tenantId}/forecasts/batch/${batchId}/status`); - } - - /** - * Get Batch Forecasts - */ - async getBatchForecasts( - tenantId: string, - params?: BaseQueryParams & { - status?: string; - start_date?: string; - end_date?: string; - } - ): Promise> { - return apiClient.get(`/tenants/${tenantId}/forecasts/batch`, { params }); - } - - /** - * Cancel Batch Forecast - */ - async cancelBatchForecast(tenantId: string, batchId: string): Promise<{ message: string }> { - return apiClient.post(`/tenants/${tenantId}/forecasts/batch/${batchId}/cancel`); - } - - /** - * Get Quick Forecasts for Dashboard - */ - async getQuickForecasts(tenantId: string, limit?: number): Promise { - try { - // TODO: Replace with actual /forecasts/quick endpoint when available - // For now, use regular forecasts endpoint and transform the data - const forecasts = await apiClient.get(`/tenants/${tenantId}/forecasts`, { - params: { limit: limit || 10 }, - }); - - // Transform regular forecasts to QuickForecast format - // Handle response structure: { tenant_id, forecasts: [...], total_returned } - let forecastsArray: any[] = []; - - if (Array.isArray(forecasts)) { - // Direct array response (unexpected) - forecastsArray = forecasts; - } else if (forecasts && typeof forecasts === 'object' && Array.isArray(forecasts.forecasts)) { - // Expected object response with forecasts array - forecastsArray = forecasts.forecasts; - } else { - console.warn('Unexpected forecasts response format:', forecasts); - return []; - } - - return forecastsArray.map((forecast: any) => ({ - inventory_product_id: forecast.inventory_product_id, - product_name: forecast.product_name, // Optional - for display - next_day_prediction: forecast.predicted_demand || 0, - next_week_avg: forecast.predicted_demand || 0, - trend_direction: 'stable' as const, - confidence_score: forecast.confidence_level || 0.8, - last_updated: forecast.created_at || new Date().toISOString() - })); - } catch (error) { - console.error('QuickForecasts API call failed, using fallback data:', error); - - // Return mock data for common bakery products (using mock inventory_product_ids) - return [ - { - inventory_product_id: 'mock-pan-de-molde-001', - product_name: 'Pan de Molde', - next_day_prediction: 25, - next_week_avg: 175, - trend_direction: 'stable', - confidence_score: 0.85, - last_updated: new Date().toISOString() - }, - { - inventory_product_id: 'mock-baguettes-002', - product_name: 'Baguettes', - next_day_prediction: 20, - next_week_avg: 140, - trend_direction: 'up', - confidence_score: 0.92, - last_updated: new Date().toISOString() - }, - { - inventory_product_id: 'mock-croissants-003', - product_name: 'Croissants', - next_day_prediction: 15, - next_week_avg: 105, - trend_direction: 'stable', - confidence_score: 0.78, - last_updated: new Date().toISOString() - }, - { - inventory_product_id: 'mock-magdalenas-004', - product_name: 'Magdalenas', - next_day_prediction: 12, - next_week_avg: 84, - trend_direction: 'down', - confidence_score: 0.76, - last_updated: new Date().toISOString() - } - ]; - } - } - - /** - * Get Forecast Alerts - */ - async getForecastAlerts( - tenantId: string, - params?: BaseQueryParams & { - is_active?: boolean; - severity?: string; - alert_type?: string; - } - ): Promise> { - return apiClient.get(`/tenants/${tenantId}/forecasts/alerts`, { params }); - } - - /** - * Acknowledge Forecast Alert - */ - async acknowledgeForecastAlert( - tenantId: string, - alertId: string - ): Promise { - return apiClient.post(`/tenants/${tenantId}/forecasts/alerts/${alertId}/acknowledge`); - } - - /** - * Delete Forecast - */ - async deleteForecast(tenantId: string, forecastId: string): Promise<{ message: string }> { - return apiClient.delete(`/tenants/${tenantId}/forecasts/${forecastId}`); - } - - /** - * Export Forecasts - */ - async exportForecasts( - tenantId: string, - format: 'csv' | 'excel' | 'json', - params?: { - inventory_product_id?: string; // Primary way to filter by product - product_name?: string; // For backward compatibility - start_date?: string; - end_date?: string; - } - ): Promise { - const response = await apiClient.request(`/tenants/${tenantId}/forecasts/export`, { - method: 'GET', - params: { ...params, format }, - headers: { - 'Accept': format === 'csv' ? 'text/csv' : - format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : - 'application/json', - }, - }); - - return new Blob([response], { - type: format === 'csv' ? 'text/csv' : - format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : - 'application/json', - }); - } - - /** - * Get Forecast Accuracy Metrics - */ - async getForecastAccuracy( - tenantId: string, - params?: { - inventory_product_id?: string; // Primary way to filter by product - product_name?: string; // For backward compatibility - model_id?: string; - start_date?: string; - end_date?: string; - } - ): Promise<{ - overall_accuracy: number; - product_accuracy: Array<{ - inventory_product_id: string; - product_name?: string; // Optional - for display - accuracy: number; - sample_size: number; - }>; - }> { - return apiClient.get(`/tenants/${tenantId}/forecasts/accuracy`, { params }); - } -} - -export const forecastingService = new ForecastingService(); \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/index.ts b/fdev-ffrontend/src/api/services/index.ts deleted file mode 100644 index bb993705..00000000 --- a/fdev-ffrontend/src/api/services/index.ts +++ /dev/null @@ -1,145 +0,0 @@ -// frontend/src/api/services/index.ts -/** - * Main Services Export - * Central export point for all API services - */ - -// Import and export individual services -import { AuthService } from './auth.service'; -import { TenantService } from './tenant.service'; -import { SalesService } from './sales.service'; -import { ExternalService } from './external.service'; -import { TrainingService } from './training.service'; -import { ForecastingService } from './forecasting.service'; -import { NotificationService } from './notification.service'; -import { OnboardingService } from './onboarding.service'; -import { InventoryService } from './inventory.service'; -import { RecipesService } from './recipes.service'; -import { ProductionService } from './production.service'; -import { OrdersService } from './orders.service'; -import { SuppliersService } from './suppliers.service'; -import { ProcurementService } from './procurement.service'; - -// Create service instances -export const authService = new AuthService(); -export const tenantService = new TenantService(); -export const salesService = new SalesService(); -export const externalService = new ExternalService(); -export const trainingService = new TrainingService(); -export const forecastingService = new ForecastingService(); -export const notificationService = new NotificationService(); -export const onboardingService = new OnboardingService(); -export const inventoryService = new InventoryService(); -export const recipesService = new RecipesService(); -export const productionService = new ProductionService(); -export const ordersService = new OrdersService(); -export const suppliersService = new SuppliersService(); -export const procurementService = new ProcurementService(); - -// Export the classes as well -export { - AuthService, - TenantService, - SalesService, - ExternalService, - TrainingService, - ForecastingService, - NotificationService, - OnboardingService, - InventoryService, - RecipesService, - ProductionService, - OrdersService, - SuppliersService, - ProcurementService -}; - -// Import base client -import { apiClient } from '../client'; -export { apiClient }; - -// Re-export all types -export * from '../types'; - -// Create unified API object -export const api = { - auth: authService, - tenant: tenantService, - sales: salesService, - external: externalService, - training: trainingService, - forecasting: forecastingService, - notification: notificationService, - onboarding: onboardingService, - inventory: inventoryService, - recipes: recipesService, - production: productionService, - orders: ordersService, - suppliers: suppliersService, - procurement: procurementService, -} as const; - -// Service status checking -export interface ServiceHealth { - service: string; - status: 'healthy' | 'degraded' | 'down'; - lastChecked: Date; - responseTime?: number; - error?: string; -} - -export class HealthService { - async checkServiceHealth(): Promise { - const services = [ - { name: 'Auth', endpoint: '/auth/health' }, - { name: 'Tenant', endpoint: '/tenants/health' }, - { name: 'Sales', endpoint: '/sales/health' }, - { name: 'External', endpoint: '/external/health' }, - { name: 'Training', endpoint: '/training/health' }, - { name: 'Inventory', endpoint: '/inventory/health' }, - { name: 'Production', endpoint: '/production/health' }, - { name: 'Orders', endpoint: '/orders/health' }, - { name: 'Suppliers', endpoint: '/suppliers/health' }, - { name: 'Forecasting', endpoint: '/forecasting/health' }, - { name: 'Notification', endpoint: '/notifications/health' }, - { name: 'Procurement', endpoint: '/procurement-plans/health' }, - ]; - - const healthChecks = await Promise.allSettled( - services.map(async (service) => { - const startTime = Date.now(); - try { - await apiClient.get(service.endpoint, { timeout: 5000 }); - const responseTime = Date.now() - startTime; - - return { - service: service.name, - status: 'healthy' as const, - lastChecked: new Date(), - responseTime, - }; - } catch (error) { - return { - service: service.name, - status: 'down' as const, - lastChecked: new Date(), - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - }) - ); - - return healthChecks.map((result, index) => - result.status === 'fulfilled' - ? result.value - : { - service: services[index].name, - status: 'down' as const, - lastChecked: new Date(), - error: 'Health check failed', - } - ); - } -} - -export const healthService = new HealthService(); \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/inventory.service.ts b/fdev-ffrontend/src/api/services/inventory.service.ts deleted file mode 100644 index 37a75d3e..00000000 --- a/fdev-ffrontend/src/api/services/inventory.service.ts +++ /dev/null @@ -1,749 +0,0 @@ -// frontend/src/api/services/inventory.service.ts -/** - * Inventory Service - * Handles inventory management, stock tracking, and product operations - */ - -import { apiClient } from '../client'; -import type { ProductInfo } from '../types'; - -// ========== TYPES AND INTERFACES ========== - -export type ProductType = 'ingredient' | 'finished_product'; - -export type UnitOfMeasure = - | 'kilograms' | 'grams' | 'liters' | 'milliliters' - | 'units' | 'pieces' | 'dozens' | 'boxes'; - -export type IngredientCategory = - | 'flour' | 'yeast' | 'dairy' | 'eggs' | 'sugar' - | 'fats' | 'salt' | 'spices' | 'additives' | 'packaging'; - -export type ProductCategory = - | 'bread' | 'croissants' | 'pastries' | 'cakes' - | 'cookies' | 'muffins' | 'sandwiches' | 'beverages' | 'other_products'; - -export type StockMovementType = - | 'purchase' | 'consumption' | 'adjustment' - | 'waste' | 'transfer' | 'return'; - -export interface InventoryItem { - id: string; - tenant_id: string; - name: string; - product_type: ProductType; - category: IngredientCategory | ProductCategory; - unit_of_measure: UnitOfMeasure; - estimated_shelf_life_days?: number; - requires_refrigeration: boolean; - requires_freezing: boolean; - is_seasonal: boolean; - minimum_stock_level?: number; - maximum_stock_level?: number; - reorder_point?: number; - supplier?: string; - notes?: string; - barcode?: string; - sku?: string; - cost_per_unit?: number; - is_active: boolean; - created_at: string; - updated_at: string; - - // Computed fields - current_stock?: StockLevel; - low_stock_alert?: boolean; - expiring_soon_alert?: boolean; - recent_movements?: StockMovement[]; -} - -export interface StockLevel { - item_id: string; - current_quantity: number; - available_quantity: number; - reserved_quantity: number; - unit_of_measure: UnitOfMeasure; - value_estimate?: number; - last_updated: string; - - // Batch information - batches?: StockBatch[]; - oldest_batch_date?: string; - newest_batch_date?: string; -} - -export interface StockBatch { - id: string; - item_id: string; - batch_number?: string; - quantity: number; - unit_cost?: number; - purchase_date?: string; - expiration_date?: string; - supplier?: string; - notes?: string; - is_expired: boolean; - days_until_expiration?: number; -} - -export interface StockMovement { - id: string; - item_id: string; - movement_type: StockMovementType; - quantity: number; - unit_cost?: number; - total_cost?: number; - batch_id?: string; - reference_id?: string; - notes?: string; - movement_date: string; - created_by: string; - created_at: string; - - // Related data - item_name?: string; - batch_info?: StockBatch; -} - - -// ========== REQUEST/RESPONSE TYPES ========== - -export interface CreateInventoryItemRequest { - name: string; - product_type: ProductType; - category: IngredientCategory | ProductCategory; - unit_of_measure: UnitOfMeasure; - estimated_shelf_life_days?: number; - requires_refrigeration?: boolean; - requires_freezing?: boolean; - is_seasonal?: boolean; - minimum_stock_level?: number; - maximum_stock_level?: number; - reorder_point?: number; - supplier?: string; - notes?: string; - barcode?: string; - cost_per_unit?: number; -} - -export interface UpdateInventoryItemRequest extends Partial { - is_active?: boolean; -} - -export interface StockAdjustmentRequest { - movement_type: StockMovementType; - quantity: number; - unit_cost?: number; - batch_number?: string; - expiration_date?: string; - supplier?: string; - notes?: string; -} - -export interface InventorySearchParams { - search?: string; - product_type?: ProductType; - category?: string; - is_active?: boolean; - low_stock_only?: boolean; - expiring_soon_only?: boolean; - page?: number; - limit?: number; - sort_by?: 'name' | 'category' | 'stock_level' | 'last_movement' | 'created_at'; - sort_order?: 'asc' | 'desc'; -} - -export interface StockMovementSearchParams { - item_id?: string; - movement_type?: StockMovementType; - date_from?: string; - date_to?: string; - page?: number; - limit?: number; -} - -export interface InventoryDashboardData { - total_items: number; - total_value: number; - low_stock_count: number; - expiring_soon_count: number; - recent_movements: StockMovement[]; - top_items_by_value: InventoryItem[]; - category_breakdown: { - category: string; - count: number; - value: number; - }[]; - movement_trends: { - date: string; - purchases: number; - consumption: number; - waste: number; - }[]; -} - -export interface PaginatedResponse { - items: T[]; - total: number; - page: number; - limit: number; - total_pages: number; -} - -// ========== INVENTORY SERVICE CLASS ========== - -export class InventoryService { - private baseEndpoint = ''; - - // ========== INVENTORY ITEMS ========== - - /** - * Get inventory items with filtering and pagination - */ - async getInventoryItems( - tenantId: string, - params?: InventorySearchParams - ): Promise> { - const searchParams = new URLSearchParams(); - - if (params) { - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - searchParams.append(key, value.toString()); - } - }); - } - - const query = searchParams.toString(); - const url = `/tenants/${tenantId}/ingredients${query ? `?${query}` : ''}`; - - console.log('🔍 InventoryService: Fetching inventory items from:', url); - - try { - console.log('🔑 InventoryService: Making request with auth token:', localStorage.getItem('auth_token') ? 'Present' : 'Missing'); - const response = await apiClient.get(url); - console.log('📋 InventoryService: Raw response:', response); - console.log('📋 InventoryService: Response type:', typeof response); - console.log('📋 InventoryService: Response keys:', response ? Object.keys(response) : 'null'); - - // Handle different response formats - if (Array.isArray(response)) { - // Direct array response - console.log('✅ InventoryService: Array response with', response.length, 'items'); - return { - items: response, - total: response.length, - page: 1, - limit: response.length, - total_pages: 1 - }; - } else if (response && typeof response === 'object') { - // Check if it's already paginated - if ('items' in response && Array.isArray(response.items)) { - console.log('✅ InventoryService: Paginated response with', response.items.length, 'items'); - return response; - } - - // Handle object with numeric keys (convert to array) - const keys = Object.keys(response); - if (keys.length > 0 && keys.every(key => !isNaN(Number(key)))) { - const items = Object.values(response); - console.log('✅ InventoryService: Numeric keys response with', items.length, 'items'); - return { - items, - total: items.length, - page: 1, - limit: items.length, - total_pages: 1 - }; - } - - // Handle empty object - this seems to be what we're getting - if (keys.length === 0) { - console.log('📭 InventoryService: Empty object response - backend has no inventory items for this tenant'); - throw new Error('NO_INVENTORY_ITEMS'); // This will trigger fallback in useInventory - } - } - - // Fallback: unexpected response format - console.warn('⚠️ InventoryService: Unexpected response format, keys:', Object.keys(response || {})); - throw new Error('UNEXPECTED_RESPONSE_FORMAT'); - } catch (error) { - console.error('❌ InventoryService: Failed to fetch inventory items:', error); - throw error; - } - } - - /** - * Get single inventory item by ID - */ - async getInventoryItem(tenantId: string, itemId: string): Promise { - return apiClient.get(`/tenants/${tenantId}/ingredients/${itemId}`); - } - - /** - * Create new inventory item - */ - async createInventoryItem( - tenantId: string, - data: CreateInventoryItemRequest - ): Promise { - return apiClient.post(`/tenants/${tenantId}/ingredients`, data); - } - - /** - * Update existing inventory item - */ - async updateInventoryItem( - tenantId: string, - itemId: string, - data: UpdateInventoryItemRequest - ): Promise { - return apiClient.put(`/tenants/${tenantId}/ingredients/${itemId}`, data); - } - - /** - * Delete inventory item (soft delete) - */ - async deleteInventoryItem(tenantId: string, itemId: string): Promise { - return apiClient.delete(`/tenants/${tenantId}/ingredients/${itemId}`); - } - - /** - * Bulk update inventory items - */ - async bulkUpdateInventoryItems( - tenantId: string, - updates: { id: string; data: UpdateInventoryItemRequest }[] - ): Promise<{ success: number; failed: number; errors: string[] }> { - return apiClient.post(`/tenants/${tenantId}/ingredients/bulk-update`, { - updates - }); - } - - // ========== STOCK MANAGEMENT ========== - - /** - * Get current stock level for an item - */ - async getStockLevel(tenantId: string, itemId: string): Promise { - return apiClient.get(`/tenants/${tenantId}/ingredients/${itemId}/stock`); - } - - /** - * Get stock levels for all items - */ - async getAllStockLevels(tenantId: string): Promise { - // TODO: Map to correct endpoint when available - return []; - // return apiClient.get(`/stock/summary`); - } - - /** - * Adjust stock level (purchase, consumption, waste, etc.) - */ - async adjustStock( - tenantId: string, - itemId: string, - adjustment: StockAdjustmentRequest - ): Promise { - return apiClient.post( - `/stock/consume`, - adjustment - ); - } - - /** - * Bulk stock adjustments - */ - async bulkAdjustStock( - tenantId: string, - adjustments: { item_id: string; adjustment: StockAdjustmentRequest }[] - ): Promise<{ success: number; failed: number; movements: StockMovement[]; errors: string[] }> { - return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/stock/bulk-adjust`, { - adjustments - }); - } - - /** - * Get stock movements with filtering - */ - async getStockMovements( - tenantId: string, - params?: StockMovementSearchParams - ): Promise> { - const searchParams = new URLSearchParams(); - - if (params) { - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - searchParams.append(key, value.toString()); - } - }); - } - - const query = searchParams.toString(); - const url = `${this.baseEndpoint}/tenants/${tenantId}/inventory/movements${query ? `?${query}` : ''}`; - - return apiClient.get(url); - } - - - // ========== DASHBOARD & ANALYTICS ========== - - - /** - * Get inventory value report - */ - async getInventoryValue(tenantId: string): Promise<{ - total_value: number; - by_category: { category: string; value: number; percentage: number }[]; - by_product_type: { type: ProductType; value: number; percentage: number }[]; - }> { - return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/value`); - } - - /** - * Get low stock report - */ - async getLowStockReport(tenantId: string): Promise<{ - items: InventoryItem[]; - total_affected: number; - estimated_loss: number; - }> { - return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/reports/low-stock`); - } - - /** - * Get expiring items report - */ - async getExpiringItemsReport(tenantId: string, days?: number): Promise<{ - items: (InventoryItem & { batches: StockBatch[] })[]; - total_affected: number; - estimated_loss: number; - }> { - const params = days ? `?days=${days}` : ''; - return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/reports/expiring${params}`); - } - - // ========== IMPORT/EXPORT ========== - - /** - * Export inventory data to CSV - */ - async exportInventory(tenantId: string, format: 'csv' | 'excel' = 'csv'): Promise { - const response = await apiClient.getRaw( - `${this.baseEndpoint}/tenants/${tenantId}/inventory/export?format=${format}` - ); - return response.blob(); - } - - /** - * Import inventory from file - */ - async importInventory(tenantId: string, file: File): Promise<{ - success: number; - failed: number; - errors: string[]; - created_items: InventoryItem[]; - }> { - const formData = new FormData(); - formData.append('file', file); - - return apiClient.post(`${this.baseEndpoint}/tenants/${tenantId}/inventory/import`, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - } - - // ========== SEARCH & SUGGESTIONS ========== - - /** - * Search inventory items with autocomplete - */ - async searchItems(tenantId: string, query: string, limit = 10): Promise { - return apiClient.get( - `${this.baseEndpoint}/tenants/${tenantId}/inventory/search?q=${encodeURIComponent(query)}&limit=${limit}` - ); - } - - /** - * Get category suggestions based on product type - */ - async getCategorySuggestions(productType: ProductType): Promise { - return apiClient.get(`${this.baseEndpoint}/inventory/categories?type=${productType}`); - } - - /** - * Get supplier suggestions - */ - async getSupplierSuggestions(tenantId: string): Promise { - return apiClient.get(`${this.baseEndpoint}/tenants/${tenantId}/inventory/suppliers`); - } - - // ========== PRODUCTS FOR FORECASTING ========== - - /** - * Get Products List with IDs for Forecasting - */ - async getProductsList(tenantId: string): Promise { - try { - console.log('🔍 Fetching products for forecasting...', { tenantId }); - - // First try to get finished products (preferred for forecasting) - const response = await apiClient.get(`/tenants/${tenantId}/ingredients`, { - params: { - limit: 100, - product_type: 'finished_product' - }, - }); - - console.log('🔍 Inventory Products API Response:', response); - console.log('🔍 Raw response data:', response.data); - console.log('🔍 Response status:', response.status); - console.log('🔍 Response headers:', response.headers); - console.log('🔍 Full response object keys:', Object.keys(response || {})); - console.log('🔍 Response data type:', typeof response); - console.log('🔍 Response data constructor:', response?.constructor?.name); - - // Check if response.data exists and what type it is - if (response && 'data' in response) { - console.log('🔍 Response.data exists:', typeof response.data); - console.log('🔍 Response.data keys:', Object.keys(response.data || {})); - console.log('🔍 Response.data constructor:', response.data?.constructor?.name); - } - - let productsArray: any[] = []; - - // Check response.data first (typical API client behavior) - const dataToProcess = response?.data || response; - - if (Array.isArray(dataToProcess)) { - productsArray = dataToProcess; - console.log('✅ Found array data with', productsArray.length, 'items'); - } else if (dataToProcess && typeof dataToProcess === 'object') { - // Handle different response formats - const keys = Object.keys(dataToProcess); - if (keys.length > 0 && keys.every(key => !isNaN(Number(key)))) { - productsArray = Object.values(dataToProcess); - console.log('✅ Found object with numeric keys, converted to array with', productsArray.length, 'items'); - } else { - console.warn('⚠️ Response is object but not with numeric keys:', dataToProcess); - console.warn('⚠️ Object keys:', keys); - return []; - } - } else { - console.warn('⚠️ Response data is not array or object:', dataToProcess); - return []; - } - - // Convert to ProductInfo objects - const products: ProductInfo[] = productsArray - .map((product: any) => ({ - inventory_product_id: product.id || product.inventory_product_id, - name: product.name || product.product_name || `Product ${product.id || ''}`, - category: product.category, - // Add additional fields if available from inventory - current_stock: product.current_stock, - unit: product.unit, - cost_per_unit: product.cost_per_unit - })) - .filter(product => product.inventory_product_id && product.name); - - console.log('📋 Processed finished products:', products); - - // If no finished products found, try to get all products as fallback - if (products.length === 0) { - console.log('⚠️ No finished products found, trying to get all products as fallback...'); - - const fallbackResponse = await apiClient.get(`/tenants/${tenantId}/ingredients`, { - params: { - limit: 100, - // No product_type filter to get all products - }, - }); - - console.log('🔍 Fallback API Response:', fallbackResponse); - - const fallbackDataToProcess = fallbackResponse?.data || fallbackResponse; - let fallbackProductsArray: any[] = []; - - if (Array.isArray(fallbackDataToProcess)) { - fallbackProductsArray = fallbackDataToProcess; - } else if (fallbackDataToProcess && typeof fallbackDataToProcess === 'object') { - const keys = Object.keys(fallbackDataToProcess); - if (keys.length > 0 && keys.every(key => !isNaN(Number(key)))) { - fallbackProductsArray = Object.values(fallbackDataToProcess); - } - } - - const fallbackProducts: ProductInfo[] = fallbackProductsArray - .map((product: any) => ({ - inventory_product_id: product.id || product.inventory_product_id, - name: product.name || product.product_name || `Product ${product.id || ''}`, - category: product.category, - current_stock: product.current_stock, - unit: product.unit, - cost_per_unit: product.cost_per_unit - })) - .filter(product => product.inventory_product_id && product.name); - - console.log('📋 Processed fallback products (all inventory items):', fallbackProducts); - return fallbackProducts; - } - - return products; - - } catch (error) { - console.error('❌ Failed to fetch inventory products:', error); - console.error('❌ Error details:', { - message: error instanceof Error ? error.message : 'Unknown error', - response: (error as any)?.response, - status: (error as any)?.response?.status, - data: (error as any)?.response?.data - }); - - // If it's an authentication error, throw it to trigger auth flow - if ((error as any)?.response?.status === 401) { - throw error; - } - - // Return empty array on other errors - let dashboard handle fallback - return []; - } - } - - /** - * Get Product by ID - */ - async getProductById(tenantId: string, productId: string): Promise { - try { - const response = await apiClient.get(`/tenants/${tenantId}/ingredients/${productId}`); - - if (response) { - return { - inventory_product_id: response.id || response.inventory_product_id, - name: response.name || response.product_name, - category: response.category, - current_stock: response.current_stock, - unit: response.unit, - cost_per_unit: response.cost_per_unit - }; - } - - return null; - } catch (error) { - console.error('❌ Failed to fetch product by ID:', error); - return null; - } - } - - // ========== ENHANCED DASHBOARD FEATURES ========== - - /** - * Get inventory dashboard data with analytics - */ - async getDashboardData(tenantId: string, params?: { - date_from?: string; - date_to?: string; - location?: string; - }): Promise<{ - summary: { - total_items: number; - low_stock_count: number; - out_of_stock_items: number; - expiring_soon: number; - total_value: number; - }; - recent_movements: any[]; - active_alerts: any[]; - stock_trends: { - dates: string[]; - stock_levels: number[]; - movements_in: number[]; - movements_out: number[]; - }; - }> { - try { - return await apiClient.get(`/tenants/${tenantId}/inventory/dashboard`, { params }); - } catch (error) { - console.error('❌ Error fetching inventory dashboard:', error); - throw error; - } - } - - /** - * Get food safety compliance data - */ - async getFoodSafetyCompliance(tenantId: string): Promise<{ - compliant_items: number; - non_compliant_items: number; - expiring_items: any[]; - temperature_violations: any[]; - compliance_score: number; - }> { - try { - return await apiClient.get(`/tenants/${tenantId}/inventory/food-safety/compliance`); - } catch (error) { - console.error('❌ Error fetching food safety compliance:', error); - throw error; - } - } - - /** - * Get temperature monitoring data - */ - async getTemperatureMonitoring(tenantId: string, params?: { - item_id?: string; - location?: string; - date_from?: string; - date_to?: string; - }): Promise<{ - readings: any[]; - violations: any[]; - }> { - try { - return await apiClient.get(`/tenants/${tenantId}/inventory/food-safety/temperature-monitoring`, { params }); - } catch (error) { - console.error('❌ Error fetching temperature monitoring:', error); - throw error; - } - } - - /** - * Record temperature reading - */ - async recordTemperatureReading(tenantId: string, params: { - item_id: string; - temperature: number; - humidity?: number; - location: string; - notes?: string; - }): Promise { - try { - return await apiClient.post(`/tenants/${tenantId}/inventory/food-safety/temperature-reading`, params); - } catch (error) { - console.error('❌ Error recording temperature reading:', error); - throw error; - } - } - - - /** - * Get restock recommendations - */ - async getRestockRecommendations(tenantId: string): Promise<{ - urgent_restocks: any[]; - optimal_orders: any[]; - }> { - try { - return await apiClient.get(`/tenants/${tenantId}/inventory/forecasting/restock-recommendations`); - } catch (error) { - console.error('❌ Error fetching restock recommendations:', error); - throw error; - } - } -} - -export const inventoryService = new InventoryService(); \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/notification.service.ts b/fdev-ffrontend/src/api/services/notification.service.ts deleted file mode 100644 index ead1c16a..00000000 --- a/fdev-ffrontend/src/api/services/notification.service.ts +++ /dev/null @@ -1,185 +0,0 @@ -// frontend/src/api/services/notification.service.ts -/** - * Notification Service - * Handles notification operations - */ - -import { apiClient } from '../client'; -import type { - NotificationCreate, - NotificationResponse, - NotificationTemplate, - NotificationHistory, - NotificationStats, - BulkNotificationRequest, - BulkNotificationStatus, - PaginatedResponse, - BaseQueryParams, -} from '../types'; - -export class NotificationService { - /** - * Send Notification - */ - async sendNotification( - tenantId: string, - notification: NotificationCreate - ): Promise { - return apiClient.post(`/tenants/${tenantId}/notifications`, notification); - } - - /** - * Send Bulk Notifications - */ - async sendBulkNotifications( - tenantId: string, - request: BulkNotificationRequest - ): Promise { - return apiClient.post(`/tenants/${tenantId}/notifications/bulk`, request); - } - - /** - * Get Notifications - */ - async getNotifications( - tenantId: string, - params?: BaseQueryParams & { - channel?: string; - status?: string; - recipient_email?: string; - start_date?: string; - end_date?: string; - } - ): Promise> { - return apiClient.get(`/tenants/${tenantId}/notifications`, { params }); - } - - /** - * Get Notification by ID - */ - async getNotification(tenantId: string, notificationId: string): Promise { - return apiClient.get(`/tenants/${tenantId}/notifications/${notificationId}`); - } - - /** - * Get Notification History - */ - async getNotificationHistory( - tenantId: string, - notificationId: string - ): Promise { - return apiClient.get(`/tenants/${tenantId}/notifications/${notificationId}/history`); - } - - /** - * Cancel Scheduled Notification - */ - async cancelNotification( - tenantId: string, - notificationId: string - ): Promise<{ message: string }> { - return apiClient.post(`/tenants/${tenantId}/notifications/${notificationId}/cancel`); - } - - /** - * Get Bulk Notification Status - */ - async getBulkNotificationStatus( - tenantId: string, - batchId: string - ): Promise { - return apiClient.get(`/tenants/${tenantId}/notifications/bulk/${batchId}/status`); - } - - /** - * Get Notification Templates - */ - async getTemplates( - tenantId: string, - params?: BaseQueryParams & { - channel?: string; - is_active?: boolean; - } - ): Promise> { - return apiClient.get(`/tenants/${tenantId}/notifications/templates`, { params }); - } - - /** - * Create Notification Template - */ - async createTemplate( - tenantId: string, - template: Omit - ): Promise { - return apiClient.post(`/tenants/${tenantId}/notifications/templates`, template); - } - - /** - * Update Notification Template - */ - async updateTemplate( - tenantId: string, - templateId: string, - template: Partial - ): Promise { - return apiClient.put(`/tenants/${tenantId}/notifications/templates/${templateId}`, template); - } - - /** - * Delete Notification Template - */ - async deleteTemplate(tenantId: string, templateId: string): Promise<{ message: string }> { - return apiClient.delete(`/tenants/${tenantId}/notifications/templates/${templateId}`); - } - - /** - * Get Notification Statistics - */ - async getNotificationStats( - tenantId: string, - params?: { - start_date?: string; - end_date?: string; - channel?: string; - } - ): Promise { - return apiClient.get(`/tenants/${tenantId}/notifications/stats`, { params }); - } - - /** - * Test Notification Configuration - */ - async testNotificationConfig( - tenantId: string, - config: { - channel: string; - recipient: string; - test_message: string; - } - ): Promise<{ success: boolean; message: string }> { - return apiClient.post(`/tenants/${tenantId}/notifications/test`, config); - } - - /** - * Get User Notification Preferences - */ - async getUserPreferences(tenantId: string, userId: string): Promise> { - return apiClient.get(`/tenants/${tenantId}/notifications/preferences/${userId}`); - } - - /** - * Update User Notification Preferences - */ - async updateUserPreferences( - tenantId: string, - userId: string, - preferences: Record - ): Promise<{ message: string }> { - return apiClient.put( - `/tenants/${tenantId}/notifications/preferences/${userId}`, - preferences - ); - } -} - -export const notificationService = new NotificationService(); diff --git a/fdev-ffrontend/src/api/services/onboarding.service.ts b/fdev-ffrontend/src/api/services/onboarding.service.ts deleted file mode 100644 index ae439fad..00000000 --- a/fdev-ffrontend/src/api/services/onboarding.service.ts +++ /dev/null @@ -1,288 +0,0 @@ -// frontend/src/api/services/onboarding.service.ts -/** - * Onboarding Service - * Handles user progress tracking and onboarding flow management - */ - -import { apiClient } from '../client'; - -export interface OnboardingStepStatus { - step_name: string; - completed: boolean; - completed_at?: string; - data?: Record; -} - -export interface UserProgress { - user_id: string; - steps: OnboardingStepStatus[]; - current_step: string; - next_step?: string; - completion_percentage: number; - fully_completed: boolean; - last_updated: string; -} - -export interface UpdateStepRequest { - step_name: string; - completed: boolean; - data?: Record; -} - -export interface InventorySuggestion { - suggestion_id: string; - original_name: string; - suggested_name: string; - product_type: 'ingredient' | 'finished_product'; - category: string; - unit_of_measure: string; - confidence_score: number; - estimated_shelf_life_days?: number; - requires_refrigeration: boolean; - requires_freezing: boolean; - is_seasonal: boolean; - suggested_supplier?: string; - notes?: string; - user_approved?: boolean; - user_modifications?: Record; -} - -export interface BusinessModelAnalysis { - model: 'production' | 'retail' | 'hybrid'; - confidence: number; - ingredient_count: number; - finished_product_count: number; - ingredient_ratio: number; - recommendations: string[]; -} - -// Step 1: File validation result -export interface FileValidationResult { - is_valid: boolean; - total_records: number; - unique_products: number; - product_list: string[]; - validation_errors: any[]; - validation_warnings: any[]; - summary: Record; -} - -// Step 2: AI suggestions result -export interface ProductSuggestionsResult { - suggestions: InventorySuggestion[]; - business_model_analysis: BusinessModelAnalysis; - total_products: number; - high_confidence_count: number; - low_confidence_count: number; - processing_time_seconds: number; -} - -// Legacy support - will be deprecated -export interface OnboardingAnalysisResult { - total_products_found: number; - inventory_suggestions: InventorySuggestion[]; - business_model_analysis: BusinessModelAnalysis; - import_job_id: string; - status: string; - processed_rows: number; - errors: string[]; - warnings: string[]; -} - -export interface InventoryCreationResult { - created_items: any[]; - failed_items: any[]; - total_approved: number; - success_rate: number; -} - -export interface SalesImportResult { - import_job_id: string; - status: string; - processed_rows: number; - successful_imports: number; - failed_imports: number; - errors: string[]; - warnings: string[]; -} - -export class OnboardingService { - private baseEndpoint = '/users/me/onboarding'; - - /** - * Get user's current onboarding progress - */ - async getUserProgress(): Promise { - return apiClient.get(`${this.baseEndpoint}/progress`); - } - - /** - * Update a specific onboarding step - */ - async updateStep(data: UpdateStepRequest): Promise { - return apiClient.put(`${this.baseEndpoint}/step`, data); - } - - /** - * Mark step as completed with optional data - */ - async completeStep(stepName: string, data?: Record): Promise { - return this.updateStep({ - step_name: stepName, - completed: true, - data - }); - } - - /** - * Reset a step (mark as incomplete) - */ - async resetStep(stepName: string): Promise { - return this.updateStep({ - step_name: stepName, - completed: false - }); - } - - /** - * Get next required step for user - */ - async getNextStep(): Promise<{ step: string; data?: Record }> { - return apiClient.get(`${this.baseEndpoint}/next-step`); - } - - /** - * Complete entire onboarding process - */ - async completeOnboarding(): Promise<{ success: boolean; message: string }> { - return apiClient.post(`${this.baseEndpoint}/complete`); - } - - /** - * Check if user can access a specific step - */ - async canAccessStep(stepName: string): Promise<{ can_access: boolean; reason?: string }> { - return apiClient.get(`${this.baseEndpoint}/can-access/${stepName}`); - } - - // ========== NEW 4-STEP AUTOMATED INVENTORY CREATION METHODS ========== - - /** - * Step 1: Validate file and extract unique products - */ - async validateFileAndExtractProducts(tenantId: string, file: File): Promise { - const formData = new FormData(); - formData.append('file', file); - - return apiClient.post(`/tenants/${tenantId}/onboarding/validate-file`, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - } - - /** - * Step 2: Generate AI-powered inventory suggestions - */ - async generateInventorySuggestions( - tenantId: string, - file: File, - productList: string[] - ): Promise { - const formData = new FormData(); - formData.append('file', file); - formData.append('product_list', JSON.stringify(productList)); - - return apiClient.post(`/tenants/${tenantId}/onboarding/generate-suggestions`, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - } - - /** - * Step 3: Create inventory from approved suggestions - */ - async createInventoryFromSuggestions( - tenantId: string, - suggestions: InventorySuggestion[] - ): Promise { - return apiClient.post(`/tenants/${tenantId}/onboarding/create-inventory`, { - suggestions: suggestions.map(s => ({ - suggestion_id: s.suggestion_id, - approved: s.user_approved ?? true, - modifications: s.user_modifications || {}, - // Include full suggestion data for backend processing - original_name: s.original_name, - suggested_name: s.suggested_name, - product_type: s.product_type, - category: s.category, - unit_of_measure: s.unit_of_measure, - confidence_score: s.confidence_score, - estimated_shelf_life_days: s.estimated_shelf_life_days, - requires_refrigeration: s.requires_refrigeration, - requires_freezing: s.requires_freezing, - is_seasonal: s.is_seasonal, - suggested_supplier: s.suggested_supplier, - notes: s.notes - })) - }); - } - - /** - * Step 4: Final sales data import with inventory mapping - */ - async importSalesWithInventory( - tenantId: string, - file: File, - inventoryMapping: Record - ): Promise { - const formData = new FormData(); - formData.append('file', file); - formData.append('inventory_mapping', JSON.stringify(inventoryMapping)); - - return apiClient.post(`/tenants/${tenantId}/onboarding/import-sales`, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - } - - // ========== LEGACY METHODS (for backward compatibility) ========== - - /** - * @deprecated Use the new 4-step flow instead - * Phase 1: Analyze sales data and get AI suggestions (OLD METHOD) - */ - async analyzeSalesDataForOnboarding(tenantId: string, file: File): Promise { - // This method will use the new flow under the hood for backward compatibility - const validationResult = await this.validateFileAndExtractProducts(tenantId, file); - - if (!validationResult.is_valid) { - throw new Error(`File validation failed: ${validationResult.validation_errors.map(e => e.message || e).join(', ')}`); - } - - const suggestionsResult = await this.generateInventorySuggestions(tenantId, file, validationResult.product_list); - - // Convert to legacy format - return { - total_products_found: suggestionsResult.total_products, - inventory_suggestions: suggestionsResult.suggestions, - business_model_analysis: suggestionsResult.business_model_analysis, - import_job_id: `legacy-${Date.now()}`, - status: 'completed', - processed_rows: validationResult.total_records, - errors: validationResult.validation_errors.map(e => e.message || String(e)), - warnings: validationResult.validation_warnings.map(w => w.message || String(w)) - }; - } - - /** - * Get business model guidance based on analysis - */ - async getBusinessModelGuide(tenantId: string, model: string): Promise { - return apiClient.get(`/tenants/${tenantId}/onboarding/business-model-guide?model=${model}`); - } -} - -export const onboardingService = new OnboardingService(); \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/orders.service.ts b/fdev-ffrontend/src/api/services/orders.service.ts deleted file mode 100644 index 141db902..00000000 --- a/fdev-ffrontend/src/api/services/orders.service.ts +++ /dev/null @@ -1,349 +0,0 @@ -// ================================================================ -// frontend/src/api/services/orders.service.ts -// ================================================================ -/** - * Orders Service - API client for Orders Service endpoints - */ - -import { apiClient } from '../client'; - -// Order Types -export interface Order { - id: string; - tenant_id: string; - customer_id?: string; - customer_name?: string; - customer_email?: string; - customer_phone?: string; - order_number: string; - status: 'pending' | 'confirmed' | 'in_production' | 'ready' | 'delivered' | 'cancelled'; - order_type: 'walk_in' | 'online' | 'phone' | 'catering'; - business_model: 'individual_bakery' | 'central_bakery'; - items: OrderItem[]; - subtotal: number; - tax_amount: number; - discount_amount: number; - total_amount: number; - delivery_date?: string; - delivery_address?: string; - notes?: string; - created_at: string; - updated_at: string; -} - -export interface OrderItem { - id: string; - recipe_id: string; - recipe_name: string; - quantity: number; - unit_price: number; - total_price: number; - customizations?: Record; - production_notes?: string; -} - -export interface Customer { - id: string; - name: string; - email?: string; - phone?: string; - address?: string; - customer_type: 'individual' | 'business' | 'catering'; - preferences?: string[]; - loyalty_points?: number; - total_orders: number; - total_spent: number; - created_at: string; - updated_at: string; -} - -export interface OrderDashboardData { - summary: { - total_orders_today: number; - pending_orders: number; - orders_in_production: number; - completed_orders: number; - revenue_today: number; - average_order_value: number; - }; - recent_orders: Order[]; - peak_hours: { hour: number; orders: number }[]; - popular_items: { recipe_name: string; quantity: number }[]; - business_model_distribution: { model: string; count: number; revenue: number }[]; -} - -export interface ProcurementPlan { - id: string; - date: string; - status: 'draft' | 'approved' | 'ordered' | 'completed'; - total_cost: number; - items: ProcurementItem[]; - supplier_orders: SupplierOrder[]; - created_at: string; - updated_at: string; -} - -export interface ProcurementItem { - ingredient_id: string; - ingredient_name: string; - required_quantity: number; - current_stock: number; - quantity_to_order: number; - unit: string; - estimated_cost: number; - priority: 'low' | 'medium' | 'high' | 'critical'; - supplier_id?: string; - supplier_name?: string; -} - -export interface SupplierOrder { - supplier_id: string; - supplier_name: string; - items: ProcurementItem[]; - total_cost: number; - delivery_date?: string; - notes?: string; -} - -export interface OrderCreateRequest { - customer_id?: string; - customer_name?: string; - customer_email?: string; - customer_phone?: string; - order_type: 'walk_in' | 'online' | 'phone' | 'catering'; - business_model: 'individual_bakery' | 'central_bakery'; - items: { - recipe_id: string; - quantity: number; - customizations?: Record; - }[]; - delivery_date?: string; - delivery_address?: string; - notes?: string; -} - -export interface OrderUpdateRequest { - status?: 'pending' | 'confirmed' | 'in_production' | 'ready' | 'delivered' | 'cancelled'; - items?: { - recipe_id: string; - quantity: number; - customizations?: Record; - }[]; - delivery_date?: string; - delivery_address?: string; - notes?: string; -} - -export class OrdersService { - private readonly basePath = '/orders'; - - // Dashboard - async getDashboardData(params?: { - date_from?: string; - date_to?: string; - }): Promise { - return apiClient.get(`${this.basePath}/dashboard`, { params }); - } - - async getDashboardMetrics(params?: { - date_from?: string; - date_to?: string; - granularity?: 'hour' | 'day' | 'week' | 'month'; - }): Promise<{ - dates: string[]; - order_counts: number[]; - revenue: number[]; - average_order_values: number[]; - business_model_breakdown: { model: string; orders: number[]; revenue: number[] }[]; - }> { - return apiClient.get(`${this.basePath}/dashboard/metrics`, { params }); - } - - // Orders - async getOrders(params?: { - status?: string; - order_type?: string; - business_model?: string; - customer_id?: string; - date_from?: string; - date_to?: string; - limit?: number; - offset?: number; - }): Promise { - return apiClient.get(`${this.basePath}`, { params }); - } - - async getOrder(orderId: string): Promise { - return apiClient.get(`${this.basePath}/${orderId}`); - } - - async createOrder(order: OrderCreateRequest): Promise { - return apiClient.post(`${this.basePath}`, order); - } - - async updateOrder(orderId: string, updates: OrderUpdateRequest): Promise { - return apiClient.put(`${this.basePath}/${orderId}`, updates); - } - - async deleteOrder(orderId: string): Promise { - return apiClient.delete(`${this.basePath}/${orderId}`); - } - - async updateOrderStatus(orderId: string, status: Order['status']): Promise { - return apiClient.patch(`${this.basePath}/${orderId}/status`, { status }); - } - - async getOrderHistory(orderId: string): Promise<{ - order: Order; - status_changes: { - status: string; - timestamp: string; - user: string; - notes?: string - }[]; - }> { - return apiClient.get(`${this.basePath}/${orderId}/history`); - } - - // Customers - async getCustomers(params?: { - search?: string; - customer_type?: string; - limit?: number; - offset?: number; - }): Promise { - return apiClient.get(`${this.basePath}/customers`, { params }); - } - - async getCustomer(customerId: string): Promise { - return apiClient.get(`${this.basePath}/customers/${customerId}`); - } - - async createCustomer(customer: { - name: string; - email?: string; - phone?: string; - address?: string; - customer_type: 'individual' | 'business' | 'catering'; - preferences?: string[]; - }): Promise { - return apiClient.post(`${this.basePath}/customers`, customer); - } - - async updateCustomer(customerId: string, updates: { - name?: string; - email?: string; - phone?: string; - address?: string; - customer_type?: 'individual' | 'business' | 'catering'; - preferences?: string[]; - }): Promise { - return apiClient.put(`${this.basePath}/customers/${customerId}`, updates); - } - - async getCustomerOrders(customerId: string, params?: { - limit?: number; - offset?: number; - }): Promise { - return apiClient.get(`${this.basePath}/customers/${customerId}/orders`, { params }); - } - - // Procurement Planning - async getProcurementPlans(params?: { - status?: string; - date_from?: string; - date_to?: string; - limit?: number; - offset?: number; - }): Promise { - return apiClient.get(`${this.basePath}/procurement/plans`, { params }); - } - - async getProcurementPlan(planId: string): Promise { - return apiClient.get(`${this.basePath}/procurement/plans/${planId}`); - } - - async createProcurementPlan(params: { - date: string; - orders?: string[]; - forecast_days?: number; - }): Promise { - return apiClient.post(`${this.basePath}/procurement/plans`, params); - } - - async updateProcurementPlan(planId: string, updates: { - items?: ProcurementItem[]; - notes?: string; - }): Promise { - return apiClient.put(`${this.basePath}/procurement/plans/${planId}`, updates); - } - - async approveProcurementPlan(planId: string): Promise { - return apiClient.post(`${this.basePath}/procurement/plans/${planId}/approve`); - } - - async generateSupplierOrders(planId: string): Promise { - return apiClient.post(`${this.basePath}/procurement/plans/${planId}/generate-orders`); - } - - // Business Model Detection - async detectBusinessModel(): Promise<{ - detected_model: 'individual_bakery' | 'central_bakery'; - confidence: number; - factors: { - daily_order_volume: number; - delivery_ratio: number; - catering_ratio: number; - average_order_size: number; - }; - recommendations: string[]; - }> { - return apiClient.post(`${this.basePath}/business-model/detect`); - } - - async updateBusinessModel(model: 'individual_bakery' | 'central_bakery'): Promise { - return apiClient.put(`${this.basePath}/business-model`, { business_model: model }); - } - - // Analytics - async getOrderTrends(params?: { - date_from?: string; - date_to?: string; - granularity?: 'hour' | 'day' | 'week' | 'month'; - }): Promise<{ - dates: string[]; - order_counts: number[]; - revenue: number[]; - popular_items: { recipe_name: string; count: number }[]; - }> { - return apiClient.get(`${this.basePath}/analytics/trends`, { params }); - } - - async getCustomerAnalytics(params?: { - date_from?: string; - date_to?: string; - }): Promise<{ - new_customers: number; - returning_customers: number; - customer_retention_rate: number; - average_lifetime_value: number; - top_customers: Customer[]; - }> { - return apiClient.get(`${this.basePath}/analytics/customers`, { params }); - } - - async getSeasonalAnalysis(params?: { - date_from?: string; - date_to?: string; - }): Promise<{ - seasonal_patterns: { month: string; order_count: number; revenue: number }[]; - weekly_patterns: { day: string; order_count: number }[]; - hourly_patterns: { hour: number; order_count: number }[]; - trending_products: { recipe_name: string; growth_rate: number }[]; - }> { - return apiClient.get(`${this.basePath}/analytics/seasonal`, { params }); - } - - - -} \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/pos.service.ts b/fdev-ffrontend/src/api/services/pos.service.ts deleted file mode 100644 index 7fb7baec..00000000 --- a/fdev-ffrontend/src/api/services/pos.service.ts +++ /dev/null @@ -1,392 +0,0 @@ -// frontend/src/api/services/pos.service.ts -/** - * POS Integration API Service - * Handles all communication with the POS service backend - */ - -import { apiClient } from '../client'; - -// ============================================================================ -// TYPES & INTERFACES -// ============================================================================ - -export interface POSConfiguration { - id: string; - tenant_id: string; - pos_system: 'square' | 'toast' | 'lightspeed'; - provider_name: string; - is_active: boolean; - is_connected: boolean; - environment: 'sandbox' | 'production'; - location_id?: string; - merchant_id?: string; - sync_enabled: boolean; - sync_interval_minutes: string; - auto_sync_products: boolean; - auto_sync_transactions: boolean; - webhook_url?: string; - last_sync_at?: string; - last_successful_sync_at?: string; - last_sync_status?: 'success' | 'failed' | 'partial'; - last_sync_message?: string; - provider_settings?: Record; - last_health_check_at?: string; - health_status: 'healthy' | 'unhealthy' | 'warning' | 'unknown'; - health_message?: string; - created_at: string; - updated_at: string; - created_by?: string; - notes?: string; -} - -export interface CreatePOSConfigurationRequest { - pos_system: 'square' | 'toast' | 'lightspeed'; - provider_name: string; - environment: 'sandbox' | 'production'; - location_id?: string; - merchant_id?: string; - sync_enabled?: boolean; - sync_interval_minutes?: string; - auto_sync_products?: boolean; - auto_sync_transactions?: boolean; - notes?: string; - // Credentials - api_key?: string; - api_secret?: string; - access_token?: string; - application_id?: string; - webhook_secret?: string; -} - -export interface UpdatePOSConfigurationRequest { - provider_name?: string; - is_active?: boolean; - environment?: 'sandbox' | 'production'; - location_id?: string; - merchant_id?: string; - sync_enabled?: boolean; - sync_interval_minutes?: string; - auto_sync_products?: boolean; - auto_sync_transactions?: boolean; - notes?: string; - // Credentials (only if updating) - api_key?: string; - api_secret?: string; - access_token?: string; - application_id?: string; - webhook_secret?: string; -} - -export interface POSTransaction { - id: string; - tenant_id: string; - pos_config_id: string; - pos_system: string; - external_transaction_id: string; - external_order_id?: string; - transaction_type: 'sale' | 'refund' | 'void' | 'exchange'; - status: 'completed' | 'pending' | 'failed' | 'refunded' | 'voided'; - subtotal: number; - tax_amount: number; - tip_amount: number; - discount_amount: number; - total_amount: number; - currency: string; - payment_method?: string; - payment_status?: string; - transaction_date: string; - pos_created_at: string; - pos_updated_at?: string; - location_id?: string; - location_name?: string; - staff_id?: string; - staff_name?: string; - customer_id?: string; - customer_email?: string; - customer_phone?: string; - order_type?: string; - table_number?: string; - receipt_number?: string; - is_synced_to_sales: boolean; - sales_record_id?: string; - sync_attempted_at?: string; - sync_completed_at?: string; - sync_error?: string; - sync_retry_count: number; - is_processed: boolean; - processing_error?: string; - is_duplicate: boolean; - duplicate_of?: string; - created_at: string; - updated_at: string; - items: POSTransactionItem[]; -} - -export interface POSTransactionItem { - id: string; - transaction_id: string; - external_item_id?: string; - sku?: string; - product_name: string; - product_category?: string; - product_subcategory?: string; - quantity: number; - unit_price: number; - total_price: number; - discount_amount: number; - tax_amount: number; - modifiers?: Record; - inventory_product_id?: string; - is_mapped_to_inventory: boolean; - is_synced_to_sales: boolean; - sync_error?: string; -} - -export interface POSSyncLog { - id: string; - tenant_id: string; - pos_config_id: string; - sync_type: 'full' | 'incremental' | 'manual' | 'webhook_triggered'; - sync_direction: 'inbound' | 'outbound' | 'bidirectional'; - data_type: 'transactions' | 'products' | 'customers' | 'orders'; - pos_system: string; - status: 'started' | 'in_progress' | 'completed' | 'failed' | 'cancelled'; - started_at: string; - completed_at?: string; - duration_seconds?: number; - sync_from_date?: string; - sync_to_date?: string; - records_requested: number; - records_processed: number; - records_created: number; - records_updated: number; - records_skipped: number; - records_failed: number; - api_calls_made: number; - error_message?: string; - error_code?: string; - retry_attempt: number; - max_retries: number; - progress_percentage?: number; - revenue_synced?: number; - transactions_synced: number; - triggered_by?: string; - triggered_by_user_id?: string; - created_at: string; - updated_at: string; -} - -export interface SyncRequest { - sync_type?: 'full' | 'incremental'; - data_types?: ('transactions' | 'products' | 'customers')[]; - from_date?: string; - to_date?: string; -} - -export interface SyncStatus { - current_sync?: POSSyncLog; - last_successful_sync?: POSSyncLog; - recent_syncs: POSSyncLog[]; - sync_health: { - status: 'healthy' | 'unhealthy' | 'warning'; - success_rate: number; - average_duration_minutes: number; - last_error?: string; - }; -} - -export interface SupportedPOSSystem { - id: string; - name: string; - description: string; - features: string[]; - supported_regions: string[]; -} - -export interface POSAnalytics { - period_days: number; - total_syncs: number; - successful_syncs: number; - failed_syncs: number; - success_rate: number; - average_duration_minutes: number; - total_transactions_synced: number; - total_revenue_synced: number; - sync_frequency: { - daily_average: number; - peak_day?: string; - peak_count: number; - }; - error_analysis: { - common_errors: Array<{ error: string; count: number }>; - error_trends: Array<{ date: string; count: number }>; - }; -} - -export interface ConnectionTestResult { - status: 'success' | 'error'; - message: string; - tested_at: string; -} - -// ============================================================================ -// API FUNCTIONS -// ============================================================================ - -export const posService = { - // Configuration Management - async getConfigurations(tenantId: string, params?: { - pos_system?: string; - is_active?: boolean; - limit?: number; - offset?: number; - }): Promise<{ configurations: POSConfiguration[]; total: number }> { - const response = await apiClient.get(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations`, { - params - }); - return response.data; - }, - - async createConfiguration(tenantId: string, data: CreatePOSConfigurationRequest): Promise { - const response = await apiClient.post(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations`, data); - return response.data; - }, - - async getConfiguration(tenantId: string, configId: string): Promise { - const response = await apiClient.get(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}`); - return response.data; - }, - - async updateConfiguration(tenantId: string, configId: string, data: UpdatePOSConfigurationRequest): Promise { - const response = await apiClient.put(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}`, data); - return response.data; - }, - - async deleteConfiguration(tenantId: string, configId: string): Promise { - await apiClient.delete(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}`); - }, - - async testConnection(tenantId: string, configId: string): Promise { - const response = await apiClient.post(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}/test-connection`); - return response.data; - }, - - // Synchronization - async triggerSync(tenantId: string, configId: string, syncRequest: SyncRequest): Promise<{ - message: string; - sync_id: string; - status: string; - sync_type: string; - data_types: string[]; - estimated_duration: string; - }> { - const response = await apiClient.post(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}/sync`, syncRequest); - return response.data; - }, - - async getSyncStatus(tenantId: string, configId: string, limit?: number): Promise { - const response = await apiClient.get(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}/sync/status`, { - params: { limit } - }); - return response.data; - }, - - async getSyncLogs(tenantId: string, configId: string, params?: { - limit?: number; - offset?: number; - status?: string; - sync_type?: string; - data_type?: string; - }): Promise<{ logs: POSSyncLog[]; total: number; has_more: boolean }> { - const response = await apiClient.get(`/pos-service/api/v1/tenants/${tenantId}/pos/configurations/${configId}/sync/logs`, { - params - }); - return response.data; - }, - - // Transaction Management - async getTransactions(tenantId: string, params?: { - pos_system?: string; - start_date?: string; - end_date?: string; - status?: string; - is_synced?: boolean; - limit?: number; - offset?: number; - }): Promise<{ - transactions: POSTransaction[]; - total: number; - has_more: boolean; - summary: { - total_amount: number; - transaction_count: number; - sync_status: { - synced: number; - pending: number; - failed: number; - }; - }; - }> { - const response = await apiClient.get(`/pos-service/api/v1/tenants/${tenantId}/pos/transactions`, { - params - }); - return response.data; - }, - - async syncSingleTransaction(tenantId: string, transactionId: string, force?: boolean): Promise<{ - message: string; - transaction_id: string; - sync_status: string; - sales_record_id?: string; - }> { - const response = await apiClient.post(`/pos-service/api/v1/tenants/${tenantId}/pos/transactions/${transactionId}/sync`, - {}, { params: { force } } - ); - return response.data; - }, - - async resyncFailedTransactions(tenantId: string, daysBack: number): Promise<{ - message: string; - job_id: string; - scope: string; - estimated_transactions: number; - }> { - const response = await apiClient.post(`/pos-service/api/v1/tenants/${tenantId}/pos/data/resync`, - {}, { params: { days_back: daysBack } } - ); - return response.data; - }, - - // Analytics - async getSyncAnalytics(tenantId: string, days: number = 30): Promise { - const response = await apiClient.get(`/pos-service/api/v1/tenants/${tenantId}/pos/analytics/sync-performance`, { - params: { days } - }); - return response.data; - }, - - // System Information - async getSupportedSystems(): Promise<{ systems: SupportedPOSSystem[] }> { - const response = await apiClient.get('/pos-service/api/v1/pos/supported-systems'); - return response.data; - }, - - // Webhook Status - async getWebhookStatus(posSystem: string): Promise<{ - pos_system: string; - status: string; - endpoint: string; - supported_events: { - events: string[]; - format: string; - authentication: string; - }; - last_received?: string; - total_received: number; - }> { - const response = await apiClient.get(`/pos-service/api/v1/webhooks/${posSystem}/status`); - return response.data; - } -}; - -export default posService; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/procurement.service.ts b/fdev-ffrontend/src/api/services/procurement.service.ts deleted file mode 100644 index ab6e9d77..00000000 --- a/fdev-ffrontend/src/api/services/procurement.service.ts +++ /dev/null @@ -1,135 +0,0 @@ -// ================================================================ -// frontend/src/api/services/procurement.service.ts -// ================================================================ -/** - * Procurement Service - API client for procurement planning endpoints - */ - -import { ApiClient } from '../client'; -import type { - ProcurementPlan, - GeneratePlanRequest, - GeneratePlanResponse, - DashboardData, - ProcurementRequirement, - PaginatedProcurementPlans -} from '../types/procurement'; - -export class ProcurementService { - constructor(private client: ApiClient) {} - - // ================================================================ - // PROCUREMENT PLAN OPERATIONS - // ================================================================ - - /** - * Get the procurement plan for the current day - */ - async getCurrentPlan(): Promise { - return this.client.get('/procurement-plans/current'); - } - - /** - * Get procurement plan for a specific date - */ - async getPlanByDate(date: string): Promise { - return this.client.get(`/procurement-plans/${date}`); - } - - /** - * Get procurement plan by ID - */ - async getPlanById(planId: string): Promise { - return this.client.get(`/procurement-plans/id/${planId}`); - } - - /** - * List procurement plans with optional filters - */ - async listPlans(params?: { - status?: string; - startDate?: string; - endDate?: string; - limit?: number; - offset?: number; - }): Promise { - return this.client.get('/procurement-plans/', { params }); - } - - /** - * Generate a new procurement plan - */ - async generatePlan(request: GeneratePlanRequest): Promise { - return this.client.post('/procurement-plans/generate', request); - } - - /** - * Update procurement plan status - */ - async updatePlanStatus(planId: string, status: string): Promise { - return this.client.put(`/procurement-plans/${planId}/status`, null, { - params: { status } - }); - } - - // ================================================================ - // REQUIREMENTS OPERATIONS - // ================================================================ - - /** - * Get all requirements for a specific procurement plan - */ - async getPlanRequirements( - planId: string, - params?: { - status?: string; - priority?: string; - } - ): Promise { - return this.client.get(`/procurement-plans/${planId}/requirements`, { params }); - } - - /** - * Get all critical priority requirements - */ - async getCriticalRequirements(): Promise { - return this.client.get('/procurement-plans/requirements/critical'); - } - - // ================================================================ - // DASHBOARD OPERATIONS - // ================================================================ - - /** - * Get procurement dashboard data - */ - async getDashboardData(): Promise { - return this.client.get('/procurement-plans/dashboard/data'); - } - - // ================================================================ - // UTILITY OPERATIONS - // ================================================================ - - /** - * Manually trigger the daily scheduler - */ - async triggerDailyScheduler(): Promise<{ success: boolean; message: string; tenant_id: string }> { - return this.client.post('/procurement-plans/scheduler/trigger'); - } - - /** - * Health check for procurement service - */ - async healthCheck(): Promise<{ - status: string; - service: string; - procurement_enabled: boolean; - timestamp: string; - }> { - return this.client.get('/procurement-plans/health'); - } -} - -// Export singleton instance -export const procurementService = new ProcurementService(new ApiClient()); \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/production.service.ts b/fdev-ffrontend/src/api/services/production.service.ts deleted file mode 100644 index f7228f88..00000000 --- a/fdev-ffrontend/src/api/services/production.service.ts +++ /dev/null @@ -1,296 +0,0 @@ -// ================================================================ -// frontend/src/api/services/production.service.ts -// ================================================================ -/** - * Production Service - API client for Production Service endpoints - */ - -import { apiClient } from '../client'; - -// Production Types -export interface ProductionBatch { - id: string; - recipe_id: string; - recipe_name: string; - quantity: number; - unit: string; - status: 'scheduled' | 'in_progress' | 'completed' | 'delayed' | 'failed'; - scheduled_start: string; - actual_start?: string; - expected_end: string; - actual_end?: string; - equipment_id: string; - equipment_name: string; - operator_id: string; - operator_name: string; - temperature?: number; - humidity?: number; - quality_score?: number; - notes?: string; - created_at: string; - updated_at: string; -} - -export interface ProductionPlan { - id: string; - date: string; - total_capacity: number; - allocated_capacity: number; - efficiency_target: number; - quality_target: number; - batches: ProductionBatch[]; - status: 'draft' | 'approved' | 'in_progress' | 'completed'; - created_at: string; - updated_at: string; -} - -export interface Equipment { - id: string; - name: string; - type: string; - status: 'active' | 'idle' | 'maintenance' | 'error'; - location: string; - capacity: number; - current_batch_id?: string; - temperature?: number; - utilization: number; - last_maintenance: string; - next_maintenance: string; - created_at: string; - updated_at: string; -} - -export interface ProductionDashboardData { - summary: { - active_batches: number; - equipment_in_use: number; - current_efficiency: number; - todays_production: number; - }; - efficiency_trend: { date: string; efficiency: number }[]; - quality_trend: { date: string; quality: number }[]; - equipment_status: Equipment[]; - active_batches: ProductionBatch[]; -} - -export interface BatchCreateRequest { - recipe_id: string; - quantity: number; - scheduled_start: string; - expected_end: string; - equipment_id: string; - operator_id: string; - notes?: string; - priority?: number; -} - -export interface BatchUpdateRequest { - status?: 'scheduled' | 'in_progress' | 'completed' | 'delayed' | 'failed'; - actual_start?: string; - actual_end?: string; - temperature?: number; - humidity?: number; - quality_score?: number; - notes?: string; -} - -export interface PlanCreateRequest { - date: string; - batches: BatchCreateRequest[]; - efficiency_target?: number; - quality_target?: number; -} - -export class ProductionService { - private readonly basePath = '/production'; - - // Dashboard - async getDashboardData(params?: { - date_from?: string; - date_to?: string; - }): Promise { - return apiClient.get(`${this.basePath}/dashboard`, { params }); - } - - async getDashboardMetrics(params?: { - date_from?: string; - date_to?: string; - granularity?: 'hour' | 'day' | 'week' | 'month'; - }): Promise<{ - dates: string[]; - efficiency: number[]; - quality: number[]; - production_volume: number[]; - equipment_utilization: number[]; - }> { - return apiClient.get(`${this.basePath}/dashboard/metrics`, { params }); - } - - // Batches - async getBatches(params?: { - status?: string; - equipment_id?: string; - date_from?: string; - date_to?: string; - limit?: number; - offset?: number; - }): Promise { - return apiClient.get(`${this.basePath}/batches`, { params }); - } - - async getBatch(batchId: string): Promise { - return apiClient.get(`${this.basePath}/batches/${batchId}`); - } - - async createBatch(batch: BatchCreateRequest): Promise { - return apiClient.post(`${this.basePath}/batches`, batch); - } - - async updateBatch(batchId: string, updates: BatchUpdateRequest): Promise { - return apiClient.put(`${this.basePath}/batches/${batchId}`, updates); - } - - async deleteBatch(batchId: string): Promise { - return apiClient.delete(`${this.basePath}/batches/${batchId}`); - } - - async startBatch(batchId: string): Promise { - return apiClient.post(`${this.basePath}/batches/${batchId}/start`); - } - - async completeBatch(batchId: string, qualityScore?: number, notes?: string): Promise { - return apiClient.post(`${this.basePath}/batches/${batchId}/complete`, { - quality_score: qualityScore, - notes - }); - } - - async getBatchStatus(batchId: string): Promise<{ - status: string; - progress: number; - current_phase: string; - temperature: number; - humidity: number; - estimated_completion: string; - }> { - return apiClient.get(`${this.basePath}/batches/${batchId}/status`); - } - - // Production Plans - async getPlans(params?: { - date_from?: string; - date_to?: string; - status?: string; - limit?: number; - offset?: number; - }): Promise { - return apiClient.get(`${this.basePath}/plans`, { params }); - } - - async getPlan(planId: string): Promise { - return apiClient.get(`${this.basePath}/plans/${planId}`); - } - - async createPlan(plan: PlanCreateRequest): Promise { - return apiClient.post(`${this.basePath}/plans`, plan); - } - - async updatePlan(planId: string, updates: Partial): Promise { - return apiClient.put(`${this.basePath}/plans/${planId}`, updates); - } - - async deletePlan(planId: string): Promise { - return apiClient.delete(`${this.basePath}/plans/${planId}`); - } - - async approvePlan(planId: string): Promise { - return apiClient.post(`${this.basePath}/plans/${planId}/approve`); - } - - async optimizePlan(planId: string): Promise { - return apiClient.post(`${this.basePath}/plans/${planId}/optimize`); - } - - // Equipment - async getEquipment(params?: { - status?: string; - type?: string; - location?: string; - limit?: number; - offset?: number; - }): Promise { - return apiClient.get(`${this.basePath}/equipment`, { params }); - } - - async getEquipmentById(equipmentId: string): Promise { - return apiClient.get(`${this.basePath}/equipment/${equipmentId}`); - } - - async updateEquipment(equipmentId: string, updates: { - status?: 'active' | 'idle' | 'maintenance' | 'error'; - temperature?: number; - notes?: string; - }): Promise { - return apiClient.put(`${this.basePath}/equipment/${equipmentId}`, updates); - } - - async getEquipmentMetrics(equipmentId: string, params?: { - date_from?: string; - date_to?: string; - }): Promise<{ - utilization: number[]; - temperature: number[]; - maintenance_events: any[]; - performance_score: number; - }> { - return apiClient.get(`${this.basePath}/equipment/${equipmentId}/metrics`, { params }); - } - - async scheduleMaintenanceForEquipment(equipmentId: string, scheduledDate: string, notes?: string): Promise { - return apiClient.post(`${this.basePath}/equipment/${equipmentId}/maintenance`, { - scheduled_date: scheduledDate, - notes - }); - } - - // Analytics - async getEfficiencyTrends(params?: { - date_from?: string; - date_to?: string; - equipment_id?: string; - }): Promise<{ - dates: string[]; - efficiency: number[]; - quality: number[]; - volume: number[]; - }> { - return apiClient.get(`${this.basePath}/analytics/efficiency`, { params }); - } - - async getProductionForecast(params?: { - days?: number; - include_weather?: boolean; - }): Promise<{ - dates: string[]; - predicted_volume: number[]; - confidence_intervals: number[][]; - factors: string[]; - }> { - return apiClient.get(`${this.basePath}/analytics/forecast`, { params }); - } - - async getQualityAnalysis(params?: { - date_from?: string; - date_to?: string; - recipe_id?: string; - }): Promise<{ - average_quality: number; - quality_trend: number[]; - quality_factors: { factor: string; impact: number }[]; - recommendations: string[]; - }> { - return apiClient.get(`${this.basePath}/analytics/quality`, { params }); - } - - -} \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/recipes.service.ts b/fdev-ffrontend/src/api/services/recipes.service.ts deleted file mode 100644 index 3dea2480..00000000 --- a/fdev-ffrontend/src/api/services/recipes.service.ts +++ /dev/null @@ -1,551 +0,0 @@ -// frontend/src/api/services/recipes.service.ts -/** - * Recipe Service API Client - * Handles all recipe and production management API calls - */ - -import { apiClient } from '../client'; -import type { - PaginatedResponse, - ApiResponse, - CreateResponse, - UpdateResponse -} from '../types'; - -// Recipe Types -export interface Recipe { - id: string; - tenant_id: string; - name: string; - recipe_code?: string; - version: string; - finished_product_id: string; - description?: string; - category?: string; - cuisine_type?: string; - difficulty_level: number; - yield_quantity: number; - yield_unit: string; - prep_time_minutes?: number; - cook_time_minutes?: number; - total_time_minutes?: number; - rest_time_minutes?: number; - estimated_cost_per_unit?: number; - last_calculated_cost?: number; - cost_calculation_date?: string; - target_margin_percentage?: number; - suggested_selling_price?: number; - instructions?: Record; - preparation_notes?: string; - storage_instructions?: string; - quality_standards?: string; - serves_count?: number; - nutritional_info?: Record; - allergen_info?: Record; - dietary_tags?: Record; - batch_size_multiplier: number; - minimum_batch_size?: number; - maximum_batch_size?: number; - optimal_production_temperature?: number; - optimal_humidity?: number; - quality_check_points?: Record; - common_issues?: Record; - status: 'draft' | 'active' | 'testing' | 'archived' | 'discontinued'; - is_seasonal: boolean; - season_start_month?: number; - season_end_month?: number; - is_signature_item: boolean; - created_at: string; - updated_at: string; - created_by?: string; - updated_by?: string; - ingredients?: RecipeIngredient[]; -} - -export interface RecipeIngredient { - id: string; - tenant_id: string; - recipe_id: string; - ingredient_id: string; - quantity: number; - unit: string; - quantity_in_base_unit?: number; - alternative_quantity?: number; - alternative_unit?: string; - preparation_method?: string; - ingredient_notes?: string; - is_optional: boolean; - ingredient_order: number; - ingredient_group?: string; - substitution_options?: Record; - substitution_ratio?: number; - unit_cost?: number; - total_cost?: number; - cost_updated_at?: string; -} - -export interface CreateRecipeRequest { - name: string; - recipe_code?: string; - version?: string; - finished_product_id: string; - description?: string; - category?: string; - cuisine_type?: string; - difficulty_level?: number; - yield_quantity: number; - yield_unit: string; - prep_time_minutes?: number; - cook_time_minutes?: number; - total_time_minutes?: number; - rest_time_minutes?: number; - instructions?: Record; - preparation_notes?: string; - storage_instructions?: string; - quality_standards?: string; - serves_count?: number; - nutritional_info?: Record; - allergen_info?: Record; - dietary_tags?: Record; - batch_size_multiplier?: number; - minimum_batch_size?: number; - maximum_batch_size?: number; - optimal_production_temperature?: number; - optimal_humidity?: number; - quality_check_points?: Record; - common_issues?: Record; - is_seasonal?: boolean; - season_start_month?: number; - season_end_month?: number; - is_signature_item?: boolean; - target_margin_percentage?: number; - ingredients: CreateRecipeIngredientRequest[]; -} - -export interface CreateRecipeIngredientRequest { - ingredient_id: string; - quantity: number; - unit: string; - alternative_quantity?: number; - alternative_unit?: string; - preparation_method?: string; - ingredient_notes?: string; - is_optional?: boolean; - ingredient_order: number; - ingredient_group?: string; - substitution_options?: Record; - substitution_ratio?: number; -} - -export interface UpdateRecipeRequest { - name?: string; - recipe_code?: string; - version?: string; - description?: string; - category?: string; - cuisine_type?: string; - difficulty_level?: number; - yield_quantity?: number; - yield_unit?: string; - prep_time_minutes?: number; - cook_time_minutes?: number; - total_time_minutes?: number; - rest_time_minutes?: number; - instructions?: Record; - preparation_notes?: string; - storage_instructions?: string; - quality_standards?: string; - serves_count?: number; - nutritional_info?: Record; - allergen_info?: Record; - dietary_tags?: Record; - batch_size_multiplier?: number; - minimum_batch_size?: number; - maximum_batch_size?: number; - optimal_production_temperature?: number; - optimal_humidity?: number; - quality_check_points?: Record; - common_issues?: Record; - status?: 'draft' | 'active' | 'testing' | 'archived' | 'discontinued'; - is_seasonal?: boolean; - season_start_month?: number; - season_end_month?: number; - is_signature_item?: boolean; - target_margin_percentage?: number; - ingredients?: CreateRecipeIngredientRequest[]; -} - -export interface RecipeSearchParams { - search_term?: string; - status?: string; - category?: string; - is_seasonal?: boolean; - is_signature?: boolean; - difficulty_level?: number; - limit?: number; - offset?: number; -} - -export interface RecipeFeasibility { - recipe_id: string; - recipe_name: string; - batch_multiplier: number; - feasible: boolean; - missing_ingredients: Array<{ - ingredient_id: string; - ingredient_name: string; - required_quantity: number; - unit: string; - }>; - insufficient_ingredients: Array<{ - ingredient_id: string; - ingredient_name: string; - required_quantity: number; - available_quantity: number; - unit: string; - }>; -} - -export interface RecipeStatistics { - total_recipes: number; - active_recipes: number; - signature_recipes: number; - seasonal_recipes: number; - category_breakdown: Array<{ - category: string; - count: number; - }>; -} - -// Production Types -export interface ProductionBatch { - id: string; - tenant_id: string; - recipe_id: string; - batch_number: string; - production_date: string; - planned_start_time?: string; - actual_start_time?: string; - planned_end_time?: string; - actual_end_time?: string; - planned_quantity: number; - actual_quantity?: number; - yield_percentage?: number; - batch_size_multiplier: number; - status: 'planned' | 'in_progress' | 'completed' | 'failed' | 'cancelled'; - priority: 'low' | 'normal' | 'high' | 'urgent'; - assigned_staff?: string[]; - production_notes?: string; - quality_score?: number; - quality_notes?: string; - defect_rate?: number; - rework_required: boolean; - planned_material_cost?: number; - actual_material_cost?: number; - labor_cost?: number; - overhead_cost?: number; - total_production_cost?: number; - cost_per_unit?: number; - production_temperature?: number; - production_humidity?: number; - oven_temperature?: number; - baking_time_minutes?: number; - waste_quantity: number; - waste_reason?: string; - efficiency_percentage?: number; - customer_order_reference?: string; - pre_order_quantity?: number; - shelf_quantity?: number; - created_at: string; - updated_at: string; - created_by?: string; - completed_by?: string; - ingredient_consumptions?: ProductionIngredientConsumption[]; -} - -export interface ProductionIngredientConsumption { - id: string; - tenant_id: string; - production_batch_id: string; - recipe_ingredient_id: string; - ingredient_id: string; - stock_id?: string; - planned_quantity: number; - actual_quantity: number; - unit: string; - variance_quantity?: number; - variance_percentage?: number; - unit_cost?: number; - total_cost?: number; - consumption_time: string; - consumption_notes?: string; - staff_member?: string; - ingredient_condition?: string; - quality_impact?: string; - substitution_used: boolean; - substitution_details?: string; -} - -export interface CreateProductionBatchRequest { - recipe_id: string; - batch_number?: string; - production_date: string; - planned_start_time?: string; - planned_end_time?: string; - planned_quantity: number; - batch_size_multiplier?: number; - priority?: 'low' | 'normal' | 'high' | 'urgent'; - assigned_staff?: string[]; - production_notes?: string; - customer_order_reference?: string; - pre_order_quantity?: number; - shelf_quantity?: number; -} - -export interface UpdateProductionBatchRequest { - batch_number?: string; - production_date?: string; - planned_start_time?: string; - actual_start_time?: string; - planned_end_time?: string; - actual_end_time?: string; - planned_quantity?: number; - actual_quantity?: number; - batch_size_multiplier?: number; - status?: 'planned' | 'in_progress' | 'completed' | 'failed' | 'cancelled'; - priority?: 'low' | 'normal' | 'high' | 'urgent'; - assigned_staff?: string[]; - production_notes?: string; - quality_score?: number; - quality_notes?: string; - defect_rate?: number; - rework_required?: boolean; - labor_cost?: number; - overhead_cost?: number; - production_temperature?: number; - production_humidity?: number; - oven_temperature?: number; - baking_time_minutes?: number; - waste_quantity?: number; - waste_reason?: string; - customer_order_reference?: string; - pre_order_quantity?: number; - shelf_quantity?: number; -} - -export interface ProductionBatchSearchParams { - search_term?: string; - status?: string; - priority?: string; - start_date?: string; - end_date?: string; - recipe_id?: string; - limit?: number; - offset?: number; -} - -export interface ProductionStatistics { - total_batches: number; - completed_batches: number; - failed_batches: number; - success_rate: number; - average_yield_percentage: number; - average_quality_score: number; - total_production_cost: number; - status_breakdown: Array<{ - status: string; - count: number; - }>; -} - -export class RecipesService { - private baseUrl = '/api/recipes/v1'; - - // Recipe Management - async getRecipes(tenantId: string, params?: RecipeSearchParams): Promise { - const response = await apiClient.get(`${this.baseUrl}/recipes`, { - headers: { 'X-Tenant-ID': tenantId }, - params - }); - return response; - } - - async getRecipe(tenantId: string, recipeId: string): Promise { - const response = await apiClient.get(`${this.baseUrl}/recipes/${recipeId}`, { - headers: { 'X-Tenant-ID': tenantId } - }); - return response; - } - - async createRecipe(tenantId: string, userId: string, data: CreateRecipeRequest): Promise { - const response = await apiClient.post(`${this.baseUrl}/recipes`, data, { - headers: { - 'X-Tenant-ID': tenantId, - 'X-User-ID': userId - } - }); - return response; - } - - async updateRecipe(tenantId: string, userId: string, recipeId: string, data: UpdateRecipeRequest): Promise { - const response = await apiClient.put(`${this.baseUrl}/recipes/${recipeId}`, data, { - headers: { - 'X-Tenant-ID': tenantId, - 'X-User-ID': userId - } - }); - return response; - } - - async deleteRecipe(tenantId: string, recipeId: string): Promise { - await apiClient.delete(`${this.baseUrl}/recipes/${recipeId}`, { - headers: { 'X-Tenant-ID': tenantId } - }); - } - - async duplicateRecipe(tenantId: string, userId: string, recipeId: string, newName: string): Promise { - const response = await apiClient.post(`${this.baseUrl}/recipes/${recipeId}/duplicate`, - { new_name: newName }, - { - headers: { - 'X-Tenant-ID': tenantId, - 'X-User-ID': userId - } - } - ); - return response; - } - - async activateRecipe(tenantId: string, userId: string, recipeId: string): Promise { - const response = await apiClient.post(`${this.baseUrl}/recipes/${recipeId}/activate`, {}, { - headers: { - 'X-Tenant-ID': tenantId, - 'X-User-ID': userId - } - }); - return response; - } - - async checkRecipeFeasibility(tenantId: string, recipeId: string, batchMultiplier: number = 1.0): Promise { - const response = await apiClient.get(`${this.baseUrl}/recipes/${recipeId}/feasibility`, { - headers: { 'X-Tenant-ID': tenantId }, - params: { batch_multiplier: batchMultiplier } - }); - return response; - } - - async getRecipeStatistics(tenantId: string): Promise { - const response = await apiClient.get(`${this.baseUrl}/recipes/statistics/dashboard`, { - headers: { 'X-Tenant-ID': tenantId } - }); - return response; - } - - async getRecipeCategories(tenantId: string): Promise { - const response = await apiClient.get<{ categories: string[] }>(`${this.baseUrl}/recipes/categories/list`, { - headers: { 'X-Tenant-ID': tenantId } - }); - return response.categories; - } - - // Production Management - async getProductionBatches(tenantId: string, params?: ProductionBatchSearchParams): Promise { - const response = await apiClient.get(`${this.baseUrl}/production/batches`, { - headers: { 'X-Tenant-ID': tenantId }, - params - }); - return response; - } - - async getProductionBatch(tenantId: string, batchId: string): Promise { - const response = await apiClient.get(`${this.baseUrl}/production/batches/${batchId}`, { - headers: { 'X-Tenant-ID': tenantId } - }); - return response; - } - - async createProductionBatch(tenantId: string, userId: string, data: CreateProductionBatchRequest): Promise { - const response = await apiClient.post(`${this.baseUrl}/production/batches`, data, { - headers: { - 'X-Tenant-ID': tenantId, - 'X-User-ID': userId - } - }); - return response; - } - - async updateProductionBatch(tenantId: string, userId: string, batchId: string, data: UpdateProductionBatchRequest): Promise { - const response = await apiClient.put(`${this.baseUrl}/production/batches/${batchId}`, data, { - headers: { - 'X-Tenant-ID': tenantId, - 'X-User-ID': userId - } - }); - return response; - } - - async deleteProductionBatch(tenantId: string, batchId: string): Promise { - await apiClient.delete(`${this.baseUrl}/production/batches/${batchId}`, { - headers: { 'X-Tenant-ID': tenantId } - }); - } - - async getActiveProductionBatches(tenantId: string): Promise { - const response = await apiClient.get(`${this.baseUrl}/production/batches/active/list`, { - headers: { 'X-Tenant-ID': tenantId } - }); - return response; - } - - async startProductionBatch(tenantId: string, userId: string, batchId: string, data: { - staff_member?: string; - production_notes?: string; - ingredient_consumptions: Array<{ - recipe_ingredient_id: string; - ingredient_id: string; - stock_id?: string; - planned_quantity: number; - actual_quantity: number; - unit: string; - consumption_notes?: string; - ingredient_condition?: string; - substitution_used?: boolean; - substitution_details?: string; - }>; - }): Promise { - const response = await apiClient.post(`${this.baseUrl}/production/batches/${batchId}/start`, data, { - headers: { - 'X-Tenant-ID': tenantId, - 'X-User-ID': userId - } - }); - return response; - } - - async completeProductionBatch(tenantId: string, userId: string, batchId: string, data: { - actual_quantity: number; - quality_score?: number; - quality_notes?: string; - defect_rate?: number; - waste_quantity?: number; - waste_reason?: string; - production_notes?: string; - staff_member?: string; - }): Promise { - const response = await apiClient.post(`${this.baseUrl}/production/batches/${batchId}/complete`, data, { - headers: { - 'X-Tenant-ID': tenantId, - 'X-User-ID': userId - } - }); - return response; - } - - async getProductionStatistics(tenantId: string, startDate?: string, endDate?: string): Promise { - const response = await apiClient.get(`${this.baseUrl}/production/statistics/dashboard`, { - headers: { 'X-Tenant-ID': tenantId }, - params: { start_date: startDate, end_date: endDate } - }); - return response; - } -} \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/sales.service.ts b/fdev-ffrontend/src/api/services/sales.service.ts deleted file mode 100644 index e71cefbc..00000000 --- a/fdev-ffrontend/src/api/services/sales.service.ts +++ /dev/null @@ -1,219 +0,0 @@ -// frontend/src/api/services/sales.service.ts -/** - * Sales Data Service - * Handles sales data operations for the sales microservice - */ - -import { apiClient } from '../client'; -import { RequestTimeouts } from '../client/config'; -import type { - SalesData, - SalesValidationResult, - SalesDataQuery, - SalesDataImport, - SalesImportResult, - DashboardStats, - PaginatedResponse, - ActivityItem, -} from '../types'; - -export class SalesService { - /** - * Upload Sales History File - */ - async uploadSalesHistory( - tenantId: string, - file: File, - additionalData?: Record - ): Promise { - // Determine file format - const fileName = file.name.toLowerCase(); - let fileFormat: string; - - if (fileName.endsWith('.csv')) { - fileFormat = 'csv'; - } else if (fileName.endsWith('.json')) { - fileFormat = 'json'; - } else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) { - fileFormat = 'excel'; - } else { - fileFormat = 'csv'; // Default fallback - } - - const uploadData = { - file_format: fileFormat, - ...additionalData, - }; - - return apiClient.upload( - `/tenants/${tenantId}/sales/import`, - file, - uploadData, - { - timeout: RequestTimeouts.LONG, - } - ); - } - - /** - * Validate Sales Data - */ - async validateSalesData( - tenantId: string, - file: File - ): Promise { - const fileName = file.name.toLowerCase(); - let fileFormat: string; - - if (fileName.endsWith('.csv')) { - fileFormat = 'csv'; - } else if (fileName.endsWith('.json')) { - fileFormat = 'json'; - } else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) { - fileFormat = 'excel'; - } else { - fileFormat = 'csv'; - } - - return apiClient.upload( - `/tenants/${tenantId}/sales/import/validate`, - file, - { - file_format: fileFormat, - validate_only: true, - source: 'onboarding_upload', - }, - { - timeout: RequestTimeouts.MEDIUM, - } - ); - } - - /** - * Get Sales Data - */ - async getSalesData( - tenantId: string, - query?: SalesDataQuery - ): Promise> { - return apiClient.get(`/tenants/${tenantId}/sales`, { params: query }); - } - - /** - * Get Single Sales Record - */ - async getSalesRecord(tenantId: string, recordId: string): Promise { - return apiClient.get(`/tenants/${tenantId}/sales/${recordId}`); - } - - /** - * Update Sales Record - */ - async updateSalesRecord( - tenantId: string, - recordId: string, - data: Partial - ): Promise { - return apiClient.put(`/tenants/${tenantId}/sales/${recordId}`, data); - } - - /** - * Delete Sales Record - */ - async deleteSalesRecord(tenantId: string, recordId: string): Promise<{ message: string }> { - return apiClient.delete(`/tenants/${tenantId}/sales/${recordId}`); - } - - /** - * Get Dashboard Statistics - */ - async getDashboardStats(tenantId: string): Promise { - return apiClient.get(`/tenants/${tenantId}/sales/stats`); - } - - /** - * Get Analytics Data - */ - async getAnalytics( - tenantId: string, - params?: { - start_date?: string; - end_date?: string; - inventory_product_ids?: string[]; // Primary way to filter by products - product_names?: string[]; // For backward compatibility - will need inventory service lookup - metrics?: string[]; - } - ): Promise { - return apiClient.get(`/tenants/${tenantId}/sales/analytics`, { params }); - } - - /** - * Export Sales Data - */ - async exportSalesData( - tenantId: string, - format: 'csv' | 'excel' | 'json', - query?: SalesDataQuery - ): Promise { - const response = await apiClient.request(`/tenants/${tenantId}/sales/export`, { - method: 'GET', - params: { ...query, format }, - headers: { - 'Accept': format === 'csv' ? 'text/csv' : - format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : - 'application/json', - }, - }); - - return new Blob([response], { - type: format === 'csv' ? 'text/csv' : - format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : - 'application/json', - }); - } - - /** - * Get Recent Activity - */ - async getRecentActivity(tenantId: string, limit?: number): Promise { - return apiClient.get(`/tenants/${tenantId}/sales/activity`, { - params: { limit }, - }); - } - - - /** - * Get Sales Summary by Period - */ - async getSalesSummary( - tenantId: string, - period: 'daily' | 'weekly' | 'monthly' = 'daily' - ): Promise { - return apiClient.get(`/tenants/${tenantId}/sales/summary`, { - params: { period } - }); - } - - /** - * Get Sales Analytics - */ - async getSalesAnalytics( - tenantId: string, - startDate?: string, - endDate?: string - ): Promise<{ - total_revenue: number; - waste_reduction_percentage?: number; - forecast_accuracy?: number; - stockout_events?: number; - }> { - return apiClient.get(`/tenants/${tenantId}/sales/analytics/summary`, { - params: { - start_date: startDate, - end_date: endDate - } - }); - } -} - -export const salesService = new SalesService(); \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/suppliers.service.ts b/fdev-ffrontend/src/api/services/suppliers.service.ts deleted file mode 100644 index 8f3c6321..00000000 --- a/fdev-ffrontend/src/api/services/suppliers.service.ts +++ /dev/null @@ -1,475 +0,0 @@ -// ================================================================ -// frontend/src/api/services/suppliers.service.ts -// ================================================================ -/** - * Suppliers Service - API client for Suppliers Service endpoints - */ - -import { apiClient } from '../client'; - -// Supplier Types -export interface Supplier { - id: string; - tenant_id: string; - name: string; - contact_person?: string; - email?: string; - phone?: string; - address?: string; - supplier_type: 'ingredients' | 'packaging' | 'equipment' | 'services'; - status: 'active' | 'inactive' | 'pending_approval'; - payment_terms?: string; - lead_time_days: number; - minimum_order_value?: number; - delivery_areas: string[]; - certifications: string[]; - rating?: number; - notes?: string; - created_at: string; - updated_at: string; -} - -export interface SupplierPerformance { - supplier_id: string; - supplier_name: string; - period_start: string; - period_end: string; - metrics: { - delivery_performance: { - on_time_delivery_rate: number; - average_delay_days: number; - total_deliveries: number; - }; - quality_performance: { - quality_score: number; - defect_rate: number; - complaints_count: number; - returns_count: number; - }; - cost_performance: { - price_competitiveness: number; - cost_savings: number; - invoice_accuracy: number; - }; - service_performance: { - responsiveness_score: number; - communication_score: number; - flexibility_score: number; - }; - }; - overall_score: number; - performance_trend: 'improving' | 'stable' | 'declining'; - risk_level: 'low' | 'medium' | 'high' | 'critical'; - recommendations: string[]; -} - -// Additional types for hooks -export interface SupplierSummary { - id: string; - name: string; - supplier_type: string; - status: string; - rating?: number; - total_orders: number; - total_spent: number; - last_delivery_date?: string; -} - -export interface CreateSupplierRequest { - name: string; - contact_person?: string; - email?: string; - phone?: string; - address?: string; - supplier_type: 'ingredients' | 'packaging' | 'equipment' | 'services'; - payment_terms?: string; - lead_time_days: number; - minimum_order_value?: number; - delivery_areas: string[]; - certifications?: string[]; - notes?: string; -} - -export interface UpdateSupplierRequest extends Partial {} - -export interface SupplierSearchParams { - supplier_type?: string; - status?: string; - search?: string; - delivery_area?: string; - limit?: number; - offset?: number; -} - -export interface SupplierStatistics { - total_suppliers: number; - active_suppliers: number; - average_rating: number; - top_performing_suppliers: SupplierSummary[]; -} - -export interface PurchaseOrder { - id: string; - supplier_id: string; - supplier_name: string; - order_number: string; - status: 'pending' | 'confirmed' | 'shipped' | 'delivered' | 'cancelled'; - total_amount: number; - created_at: string; - expected_delivery: string; -} - -export interface CreatePurchaseOrderRequest { - supplier_id: string; - items: Array<{ - product_id: string; - quantity: number; - unit_price: number; - }>; - delivery_date: string; - notes?: string; -} - -export interface PurchaseOrderSearchParams { - supplier_id?: string; - status?: string; - date_from?: string; - date_to?: string; - limit?: number; - offset?: number; -} - -export interface PurchaseOrderStatistics { - total_orders: number; - total_value: number; - pending_orders: number; - overdue_orders: number; -} - -export interface Delivery { - id: string; - purchase_order_id: string; - supplier_name: string; - delivered_at: string; - status: 'on_time' | 'late' | 'early'; - quality_rating?: number; -} - -export interface DeliverySearchParams { - supplier_id?: string; - status?: string; - date_from?: string; - date_to?: string; - limit?: number; - offset?: number; -} - -export interface DeliveryPerformanceStats { - on_time_delivery_rate: number; - average_delay_days: number; - quality_average: number; - total_deliveries: number; -} - -export interface SupplierDashboardData { - summary: { - total_suppliers: number; - active_suppliers: number; - pending_orders: number; - overdue_deliveries: number; - average_performance_score: number; - total_monthly_spend: number; - }; - top_performers: SupplierPerformance[]; - recent_orders: any[]; - performance_trends: { - dates: string[]; - delivery_rates: number[]; - quality_scores: number[]; - cost_savings: number[]; - }; - contract_expirations: { supplier_name: string; days_until_expiry: number }[]; -} - -export class SuppliersService { - private readonly basePath = '/suppliers'; - - // Dashboard - async getDashboardData(params?: { - date_from?: string; - date_to?: string; - }): Promise { - return apiClient.get(`${this.basePath}/dashboard`, { params }); - } - - async getDashboardMetrics(params?: { - date_from?: string; - date_to?: string; - supplier_id?: string; - }): Promise<{ - dates: string[]; - delivery_performance: number[]; - quality_scores: number[]; - cost_savings: number[]; - order_volumes: number[]; - }> { - return apiClient.get(`${this.basePath}/dashboard/metrics`, { params }); - } - - // Suppliers - async getSuppliers(params?: { - supplier_type?: string; - status?: string; - search?: string; - delivery_area?: string; - limit?: number; - offset?: number; - }): Promise { - return apiClient.get(`${this.basePath}`, { params }); - } - - async getSupplier(supplierId: string): Promise { - return apiClient.get(`${this.basePath}/${supplierId}`); - } - - async createSupplier(supplier: { - name: string; - contact_person?: string; - email?: string; - phone?: string; - address?: string; - supplier_type: 'ingredients' | 'packaging' | 'equipment' | 'services'; - payment_terms?: string; - lead_time_days: number; - minimum_order_value?: number; - delivery_areas: string[]; - certifications?: string[]; - notes?: string; - }): Promise { - return apiClient.post(`${this.basePath}`, supplier); - } - - async updateSupplier(supplierId: string, updates: Partial): Promise { - return apiClient.put(`${this.basePath}/${supplierId}`, updates); - } - - async deleteSupplier(supplierId: string): Promise { - return apiClient.delete(`${this.basePath}/${supplierId}`); - } - - // Performance Management - async getSupplierPerformance(supplierId: string, params?: { - period_start?: string; - period_end?: string; - }): Promise { - return apiClient.get(`${this.basePath}/${supplierId}/performance`, { params }); - } - - async getAllSupplierPerformance(params?: { - period_start?: string; - period_end?: string; - min_score?: number; - risk_level?: string; - limit?: number; - offset?: number; - }): Promise { - return apiClient.get(`${this.basePath}/performance`, { params }); - } - - async updateSupplierRating(supplierId: string, rating: { - overall_rating: number; - delivery_rating: number; - quality_rating: number; - service_rating: number; - comments?: string; - }): Promise { - return apiClient.post(`${this.basePath}/${supplierId}/rating`, rating); - } - - // Analytics - async getCostAnalysis(params?: { - date_from?: string; - date_to?: string; - supplier_id?: string; - category?: string; - }): Promise<{ - total_spend: number; - cost_by_supplier: { supplier_name: string; amount: number }[]; - cost_by_category: { category: string; amount: number }[]; - cost_trends: { date: string; amount: number }[]; - cost_savings_opportunities: string[]; - }> { - return apiClient.get(`${this.basePath}/analytics/costs`, { params }); - } - - async getSupplyChainRiskAnalysis(): Promise<{ - high_risk_suppliers: { - supplier_id: string; - supplier_name: string; - risk_factors: string[]; - risk_score: number; - }[]; - diversification_analysis: { - category: string; - supplier_count: number; - concentration_risk: number; - }[]; - recommendations: string[]; - }> { - return apiClient.get(`${this.basePath}/analytics/risk-analysis`); - } - - - - // Additional methods for hooks compatibility - async getSupplierStatistics(): Promise { - const suppliers = await this.getSuppliers(); - const activeSuppliers = suppliers.filter(s => s.status === 'active'); - const averageRating = suppliers.reduce((sum, s) => sum + (s.rating || 0), 0) / suppliers.length; - - return { - total_suppliers: suppliers.length, - active_suppliers: activeSuppliers.length, - average_rating: averageRating, - top_performing_suppliers: suppliers.slice(0, 5).map(s => ({ - id: s.id, - name: s.name, - supplier_type: s.supplier_type, - status: s.status, - rating: s.rating, - total_orders: 0, // Would come from backend - total_spent: 0, // Would come from backend - last_delivery_date: undefined - })) - }; - } - - async getActiveSuppliers(): Promise { - const suppliers = await this.getSuppliers({ status: 'active' }); - return suppliers.map(s => ({ - id: s.id, - name: s.name, - supplier_type: s.supplier_type, - status: s.status, - rating: s.rating, - total_orders: 0, // Would come from backend - total_spent: 0, // Would come from backend - last_delivery_date: undefined - })); - } - - async getTopSuppliers(): Promise { - const suppliers = await this.getSuppliers(); - return suppliers - .sort((a, b) => (b.rating || 0) - (a.rating || 0)) - .slice(0, 10) - .map(s => ({ - id: s.id, - name: s.name, - supplier_type: s.supplier_type, - status: s.status, - rating: s.rating, - total_orders: 0, // Would come from backend - total_spent: 0, // Would come from backend - last_delivery_date: undefined - })); - } - - async getSuppliersNeedingReview(): Promise { - const suppliers = await this.getSuppliers(); - return suppliers - .filter(s => !s.rating || s.rating < 3) - .map(s => ({ - id: s.id, - name: s.name, - supplier_type: s.supplier_type, - status: s.status, - rating: s.rating, - total_orders: 0, // Would come from backend - total_spent: 0, // Would come from backend - last_delivery_date: undefined - })); - } - - // Purchase Order Management Methods - async getPurchaseOrders(params?: PurchaseOrderSearchParams): Promise { - return apiClient.get(`${this.basePath}/purchase-orders`, { params }); - } - - async getPurchaseOrder(orderId: string): Promise { - return apiClient.get(`${this.basePath}/purchase-orders/${orderId}`); - } - - async createPurchaseOrder(orderData: CreatePurchaseOrderRequest): Promise { - return apiClient.post(`${this.basePath}/purchase-orders`, orderData); - } - - async updatePurchaseOrderStatus(orderId: string, status: string): Promise { - return apiClient.put(`${this.basePath}/purchase-orders/${orderId}/status`, { status }); - } - - async approvePurchaseOrder(orderId: string, approval: any): Promise { - return apiClient.post(`${this.basePath}/purchase-orders/${orderId}/approve`, approval); - } - - async sendToSupplier(orderId: string): Promise { - return apiClient.post(`${this.basePath}/purchase-orders/${orderId}/send`); - } - - async cancelPurchaseOrder(orderId: string, reason?: string): Promise { - return apiClient.post(`${this.basePath}/purchase-orders/${orderId}/cancel`, { reason }); - } - - async getPurchaseOrderStatistics(): Promise { - const orders = await this.getPurchaseOrders(); - return { - total_orders: orders.length, - total_value: orders.reduce((sum, o) => sum + o.total_amount, 0), - pending_orders: orders.filter(o => o.status === 'pending').length, - overdue_orders: 0, // Would calculate based on expected delivery dates - }; - } - - async getOrdersRequiringApproval(): Promise { - return this.getPurchaseOrders({ status: 'pending' }); - } - - async getOverdueOrders(): Promise { - const today = new Date(); - const orders = await this.getPurchaseOrders(); - return orders.filter(o => new Date(o.expected_delivery) < today && o.status !== 'delivered'); - } - - // Delivery Management Methods - async getDeliveries(params?: DeliverySearchParams): Promise { - return apiClient.get(`${this.basePath}/deliveries`, { params }); - } - - async getDelivery(deliveryId: string): Promise { - return apiClient.get(`${this.basePath}/deliveries/${deliveryId}`); - } - - async getTodaysDeliveries(): Promise { - const today = new Date().toISOString().split('T')[0]; - return this.getDeliveries({ date_from: today, date_to: today }); - } - - async getDeliveryPerformanceStats(): Promise { - const deliveries = await this.getDeliveries(); - const onTimeCount = deliveries.filter(d => d.status === 'on_time').length; - const totalCount = deliveries.length; - const qualitySum = deliveries.reduce((sum, d) => sum + (d.quality_rating || 0), 0); - - return { - on_time_delivery_rate: totalCount > 0 ? (onTimeCount / totalCount) * 100 : 0, - average_delay_days: 0, // Would calculate based on actual vs expected delivery - quality_average: totalCount > 0 ? qualitySum / totalCount : 0, - total_deliveries: totalCount, - }; - } - - // Additional utility methods - async approveSupplier(supplierId: string): Promise { - await this.updateSupplier(supplierId, { status: 'active' }); - } -} \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/tenant.service.ts b/fdev-ffrontend/src/api/services/tenant.service.ts deleted file mode 100644 index c1cda969..00000000 --- a/fdev-ffrontend/src/api/services/tenant.service.ts +++ /dev/null @@ -1,150 +0,0 @@ -// frontend/src/api/services/tenant.service.ts -/** - * Tenant Management Service - * Handles all tenant-related operations - */ - -import { apiClient } from '../client'; -import { serviceEndpoints } from '../client/config'; -import type { - TenantInfo, - TenantCreate, - TenantUpdate, - TenantMember, - InviteUser, - TenantStats, - PaginatedResponse, - BaseQueryParams, -} from '../types'; - -export class TenantService { - private baseEndpoint = serviceEndpoints.tenant; - - /** - * Create New Tenant - */ - async createTenant(data: TenantCreate): Promise { - return apiClient.post(`${this.baseEndpoint}/register`, data); - } - - /** - * Get Tenant Details - */ - async getTenant(tenantId: string): Promise { - return apiClient.get(`${this.baseEndpoint}/${tenantId}`); - } - - /** - * Update Tenant - */ - async updateTenant(tenantId: string, data: TenantUpdate): Promise { - return apiClient.put(`${this.baseEndpoint}/${tenantId}`, data); - } - - /** - * Delete Tenant - */ - async deleteTenant(tenantId: string): Promise<{ message: string }> { - return apiClient.delete(`${this.baseEndpoint}/${tenantId}`); - } - - /** - * Get Tenant Members - */ - async getTenantMembers( - tenantId: string, - params?: BaseQueryParams - ): Promise> { - return apiClient.get(`${this.baseEndpoint}/${tenantId}/members`, { params }); - } - - /** - * Invite User to Tenant - */ - async inviteUser(tenantId: string, invitation: InviteUser): Promise<{ message: string }> { - return apiClient.post(`${this.baseEndpoint}/${tenantId}/invite`, invitation); - } - - /** - * Remove Member from Tenant - */ - async removeMember(tenantId: string, userId: string): Promise<{ message: string }> { - return apiClient.delete(`${this.baseEndpoint}/${tenantId}/members/${userId}`); - } - - /** - * Update Member Role - */ - async updateMemberRole( - tenantId: string, - userId: string, - role: string - ): Promise { - return apiClient.patch(`${this.baseEndpoint}/${tenantId}/members/${userId}`, { role }); - } - - /** - * Get Tenant Statistics - */ - async getTenantStats(tenantId: string): Promise { - return apiClient.get(`${this.baseEndpoint}/${tenantId}/stats`); - } - - /** - * Get User's Tenants - Get tenants where user is owner - */ - async getUserTenants(): Promise { - try { - // Extract user ID from the JWT token in localStorage - const token = localStorage.getItem('auth_token'); - console.log('🔑 TenantService: Auth token present:', !!token); - - if (!token) { - throw new Error('No auth token found'); - } - - // Decode JWT to get user ID (simple base64 decode) - const payload = JSON.parse(atob(token.split('.')[1])); - const userId = payload.user_id || payload.sub; - console.log('👤 TenantService: Extracted user ID:', userId); - - if (!userId) { - throw new Error('No user ID found in token'); - } - - // Get tenants owned by this user - const url = `${this.baseEndpoint}/user/${userId}/owned`; - console.log('🌐 TenantService: Making request to:', url); - - const result = await apiClient.get(url); - console.log('📦 TenantService: API response:', result); - console.log('📏 TenantService: Response length:', Array.isArray(result) ? result.length : 'Not an array'); - - // Ensure we always return an array - if (!Array.isArray(result)) { - console.warn('⚠️ TenantService: Response is not an array, converting...'); - // If it's an object with numeric keys, convert to array - if (result && typeof result === 'object') { - const converted = Object.values(result); - console.log('🔄 TenantService: Converted to array:', converted); - return converted as TenantInfo[]; - } - console.log('🔄 TenantService: Returning empty array'); - return []; - } - - if (result.length > 0) { - console.log('✅ TenantService: First tenant:', result[0]); - console.log('🆔 TenantService: First tenant ID:', result[0]?.id); - } - - return result; - } catch (error) { - console.error('❌ TenantService: Failed to get user tenants:', error); - // Return empty array if API call fails - return []; - } - } -} - -export const tenantService = new TenantService(); \ No newline at end of file diff --git a/fdev-ffrontend/src/api/services/training.service.ts b/fdev-ffrontend/src/api/services/training.service.ts deleted file mode 100644 index 89d86635..00000000 --- a/fdev-ffrontend/src/api/services/training.service.ts +++ /dev/null @@ -1,161 +0,0 @@ -// frontend/src/api/services/training.service.ts -/** - * Training Service - * Handles ML model training operations - */ - -import { apiClient } from '../client'; -import { RequestTimeouts } from '../client/config'; -import type { - TrainingJobRequest, - TrainingJobResponse, - SingleProductTrainingRequest, - ModelInfo, - ModelTrainingStats, - PaginatedResponse, - BaseQueryParams, -} from '../types'; - -export class TrainingService { - /** - * Start Training Job for All Products - */ - async startTrainingJob( - tenantId: string, - request: TrainingJobRequest - ): Promise { - return apiClient.post( - `/tenants/${tenantId}/training/jobs`, - request, - { - timeout: RequestTimeouts.EXTENDED, - } - ); - } - - /** - * Start Training for Single Product - */ - async startSingleProductTraining( - tenantId: string, - request: SingleProductTrainingRequest - ): Promise { - return apiClient.post( - `/tenants/${tenantId}/training/single`, - request, - { - timeout: RequestTimeouts.EXTENDED, - } - ); - } - - /** - * Get Training Job Status - */ - async getTrainingJobStatus(tenantId: string, jobId: string): Promise { - return apiClient.get(`/tenants/${tenantId}/training/jobs/${jobId}/status`); - } - - /** - * Get Training Job Logs - */ - async getTrainingJobLogs(tenantId: string, jobId: string): Promise<{ logs: string[] }> { - return apiClient.get(`/tenants/${tenantId}/training/jobs/${jobId}/logs`); - } - - /** - * Cancel Training Job - */ - async cancelTrainingJob(tenantId: string, jobId: string): Promise<{ message: string }> { - return apiClient.post(`/tenants/${tenantId}/training/jobs/${jobId}/cancel`); - } - - /** - * Get Training Jobs - */ - async getTrainingJobs( - tenantId: string, - params?: BaseQueryParams & { - status?: string; - start_date?: string; - end_date?: string; - } - ): Promise> { - return apiClient.get(`/tenants/${tenantId}/training/jobs`, { params }); - } - - /** - * Validate Data for Training - */ - async validateTrainingData(tenantId: string): Promise<{ - is_valid: boolean; - message: string; - details?: any; - }> { - return apiClient.post(`/tenants/${tenantId}/training/validate`); - } - - /** - * Get Trained Models - */ - async getModels( - tenantId: string, - params?: BaseQueryParams & { - inventory_product_id?: string; // Primary way to filter by product - product_name?: string; // For backward compatibility - will need inventory service lookup - is_active?: boolean; - } - ): Promise> { - return apiClient.get(`/tenants/${tenantId}/models`, { params }); - } - - /** - * Get Model Details - */ - async getModel(tenantId: string, modelId: string): Promise { - return apiClient.get(`/tenants/${tenantId}/models/${modelId}`); - } - - /** - * Update Model Status - */ - async updateModelStatus( - tenantId: string, - modelId: string, - isActive: boolean - ): Promise { - return apiClient.patch(`/tenants/${tenantId}/models/${modelId}`, { - is_active: isActive, - }); - } - - /** - * Delete Model - */ - async deleteModel(tenantId: string, modelId: string): Promise<{ message: string }> { - return apiClient.delete(`/tenants/${tenantId}/models/${modelId}`); - } - - /** - * Get Training Statistics - */ - async getTrainingStats(tenantId: string): Promise { - return apiClient.get(`/tenants/${tenantId}/training/stats`); - } - - /** - * Download Model File - */ - async downloadModel(tenantId: string, modelId: string): Promise { - const response = await apiClient.request(`/tenants/${tenantId}/models/${modelId}/download`, { - method: 'GET', - headers: { - 'Accept': 'application/octet-stream', - }, - }); - - return new Blob([response]); - } -} - -export const trainingService = new TrainingService(); \ No newline at end of file diff --git a/fdev-ffrontend/src/api/types/auth.ts b/fdev-ffrontend/src/api/types/auth.ts deleted file mode 100644 index 414bd925..00000000 --- a/fdev-ffrontend/src/api/types/auth.ts +++ /dev/null @@ -1,94 +0,0 @@ -// frontend/src/api/types/auth.ts -/** - * Authentication Types - */ - -export interface UserData { - id: string; - email: string; - full_name: string; - is_active: boolean; - is_verified: boolean; - created_at: string; - last_login?: string; - phone?: string; - language?: string; - timezone?: string; - tenant_id?: string; - role: string; -} - -export interface User { - id: string; - email: string; - fullName: string; - role: "owner" | "admin" | "manager" | "worker"; - isOnboardingComplete: boolean; - tenant_id: string; - created_at?: string; - last_login?: string; -} - -export interface LoginRequest { - email: string; - password: string; -} - -export interface RegisterRequest { - email: string; - password: string; - full_name: string; - role?: string; - phone?: string; - language?: string; -} - -export interface LoginResponse { - access_token: string; - refresh_token?: string; - token_type: string; - expires_in: number; - user?: UserData; -} - -export interface UserResponse { - id: string; - email: string; - full_name: string; - is_active: boolean; - is_verified: boolean; - created_at: string; - last_login?: string; - phone?: string; - language?: string; - timezone?: string; - tenant_id?: string; - role?: string; -} - -export interface TokenVerification { - valid: boolean; - user_id?: string; - email?: string; - exp?: number; - message?: string; -} - -export interface PasswordResetRequest { - email: string; -} - -export interface PasswordResetResponse { - message: string; - reset_token?: string; -} - -export interface PasswordResetConfirmRequest { - token: string; - new_password: string; -} - -export interface LogoutResponse { - message: string; - success: boolean; -} diff --git a/fdev-ffrontend/src/api/types/common.ts b/fdev-ffrontend/src/api/types/common.ts deleted file mode 100644 index e4a64e3c..00000000 --- a/fdev-ffrontend/src/api/types/common.ts +++ /dev/null @@ -1,54 +0,0 @@ -// frontend/src/api/types/common.ts -/** - * Common API Types - */ - -export interface ApiResponse { - data?: T; - message?: string; - status: string; - timestamp?: string; - meta?: PaginationMeta; -} - -export interface ApiError { - detail: string; - service?: string; - error_code?: string; - timestamp?: string; - field?: string; -} - -export interface PaginationMeta { - page: number; - limit: number; - total: number; - totalPages: number; - hasNext: boolean; - hasPrev: boolean; -} - -export interface PaginatedResponse { - data: T[]; - pagination: PaginationMeta; -} - -export interface BaseQueryParams { - page?: number; - limit?: number; - search?: string; - sort?: string; - order?: 'asc' | 'desc'; -} - -export interface CreateResponse { - data: T; - message?: string; - status: string; -} - -export interface UpdateResponse { - data: T; - message?: string; - status: string; -} \ No newline at end of file diff --git a/fdev-ffrontend/src/api/types/data.ts b/fdev-ffrontend/src/api/types/data.ts deleted file mode 100644 index fb3dfac0..00000000 --- a/fdev-ffrontend/src/api/types/data.ts +++ /dev/null @@ -1,148 +0,0 @@ -// frontend/src/api/types/data.ts -/** - * Data Management Types - */ - -import { BaseQueryParams } from './common'; - -export interface ProductInfo { - inventory_product_id: string; - name: string; - category?: string; - sales_count?: number; - total_quantity?: number; - last_sale_date?: string; - // Additional inventory fields - current_stock?: number; - unit?: string; - cost_per_unit?: number; -} - -export interface SalesData { - id: string; - tenant_id: string; - date: string; - inventory_product_id: string; // Reference to inventory service product - // Note: product_name now needs to be fetched from inventory service using inventory_product_id - product_name?: string; // Optional - for backward compatibility, populated by frontend logic - category?: string; // Optional - fetched from inventory service - quantity: number; - unit_price: number; - total_revenue: number; - location_id?: string; - source: string; - created_at: string; - external_factors?: ExternalFactors; - // Additional properties used by components - sales_channel?: string; - is_validated?: boolean; - cost_of_goods?: number; - revenue?: number; - quantity_sold?: number; - discount_applied?: number; - weather_condition?: string; -} - -export interface SalesValidationResult { - is_valid: boolean; - total_records: number; - valid_records: number; - invalid_records: number; - errors: ValidationError[]; - warnings: ValidationError[]; - summary: Record; - message?: string; - details?: Record; -} - -export interface ExternalFactors { - weather_temperature?: number; - weather_precipitation?: number; - weather_description?: string; - traffic_volume?: number; - is_holiday?: boolean; - is_weekend?: boolean; - day_of_week?: number; -} - -export interface SalesDataQuery extends BaseQueryParams { - tenant_id: string; - start_date?: string; - end_date?: string; - // Note: product_names filtering now requires inventory service integration or use inventory_product_ids - product_names?: string[]; // For backward compatibility - will need inventory service lookup - inventory_product_ids?: string[]; // Primary way to filter by products - location_ids?: string[]; - sources?: string[]; - min_quantity?: number; - max_quantity?: number; - min_revenue?: number; - max_revenue?: number; - search_term?: string; - sales_channel?: string; - inventory_product_id?: string; // Single product filter - is_validated?: boolean; -} - -export interface SalesDataImport { - tenant_id?: string; - data: string; - data_format: 'csv' | 'json' | 'excel'; - source?: string; - validate_only?: boolean; -} - -export interface SalesImportResult { - success: boolean; - message: string; - imported_count: number; - records_imported: number; - skipped_count: number; - error_count: number; - validation_errors?: ValidationError[]; - preview_data?: SalesData[]; - file_info?: FileInfo; -} - -export interface ValidationError { - row: number; - field: string; - value: any; - message: string; -} - -export interface FileInfo { - filename: string; - size: number; - rows: number; - format: string; -} - -export interface DashboardStats { - total_sales: number; - total_revenue: number; - total_products: number; - date_range: { - start: string; - end: string; - }; - top_products: ProductStats[]; - recent_activity: ActivityItem[]; -} - -export interface ProductStats { - inventory_product_id: string; // Reference to inventory service product - product_name?: string; // Optional - for display, populated by frontend from inventory service - total_quantity: number; - total_revenue: number; - avg_price: number; - sales_trend: number; -} - -export interface ActivityItem { - id: string; - type: string; - description: string; - timestamp: string; - metadata?: Record; -} \ No newline at end of file diff --git a/fdev-ffrontend/src/api/types/forecasting.ts b/fdev-ffrontend/src/api/types/forecasting.ts deleted file mode 100644 index 585fc032..00000000 --- a/fdev-ffrontend/src/api/types/forecasting.ts +++ /dev/null @@ -1,89 +0,0 @@ -// frontend/src/api/types/forecasting.ts -/** - * Forecasting Service Types - */ - -import { ExternalFactors } from './data'; - -export interface SingleForecastRequest { - inventory_product_id: string; - forecast_date: string; - forecast_days: number; - location: string; - include_external_factors?: boolean; - confidence_intervals?: boolean; - // Note: confidence_level is handled internally by backend (0.8 default) -} - -export interface BatchForecastRequest { - inventory_product_ids?: string[]; // Primary way to specify products - product_names?: string[]; // For backward compatibility - will need inventory service lookup - forecast_date: string; - forecast_days: number; - location: string; - include_external_factors?: boolean; - confidence_intervals?: boolean; - batch_name?: string; -} - -export interface ForecastResponse { - id: string; - tenant_id: string; - inventory_product_id: string; - product_name?: string; // Optional - for display, populated by frontend from inventory service - forecast_date: string; - predicted_demand: number; - confidence_lower?: number; - confidence_upper?: number; - model_id: string; - confidence_level?: number; - external_factors?: ExternalFactors; - created_at: string; - processing_time_ms?: number; - features_used?: Record; -} - -export interface BatchForecastResponse { - id: string; - tenant_id: string; - batch_name: string; - status: BatchForecastStatus; - total_products: number; - completed_products: number; - failed_products: number; - requested_at: string; - completed_at?: string; - processing_time_ms?: number; - forecasts?: ForecastResponse[]; - error_message?: string; -} - -export type BatchForecastStatus = - | 'pending' - | 'processing' - | 'completed' - | 'failed' - | 'cancelled'; - -export interface ForecastAlert { - id: string; - tenant_id: string; - forecast_id: string; - alert_type: string; - severity: 'low' | 'medium' | 'high' | 'critical'; - message: string; - is_active: boolean; - created_at: string; - acknowledged_at?: string; - notification_sent: boolean; -} - -export interface QuickForecast { - inventory_product_id: string; - product_name?: string; // Optional - for display, populated by frontend from inventory service - next_day_prediction: number; - next_week_avg: number; - trend_direction: 'up' | 'down' | 'stable'; - confidence_score: number; - last_updated: string; -} diff --git a/fdev-ffrontend/src/api/types/index.ts b/fdev-ffrontend/src/api/types/index.ts deleted file mode 100644 index 52107a9f..00000000 --- a/fdev-ffrontend/src/api/types/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -// frontend/src/api/types/index.ts -/** - * Main Types Export - */ - -// Re-export all types -export * from './common'; -export * from './auth'; -export * from './tenant'; -export * from './data'; -export * from './training'; -export * from './forecasting'; -export * from './notification'; -export * from './procurement'; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/types/notification.ts b/fdev-ffrontend/src/api/types/notification.ts deleted file mode 100644 index e395db5b..00000000 --- a/fdev-ffrontend/src/api/types/notification.ts +++ /dev/null @@ -1,108 +0,0 @@ -// frontend/src/api/types/notification.ts -/** - * Notification Service Types - */ - -export interface NotificationCreate { - recipient_id?: string; - recipient_email?: string; - recipient_phone?: string; - channel: NotificationChannel; - template_id?: string; - subject?: string; - message: string; - data?: Record; - scheduled_for?: string; - priority?: NotificationPriority; -} - -export interface NotificationResponse { - id: string; - tenant_id: string; - recipient_id?: string; - recipient_email?: string; - recipient_phone?: string; - channel: NotificationChannel; - template_id?: string; - subject?: string; - message: string; - status: NotificationStatus; - priority: NotificationPriority; - data?: Record; - scheduled_for?: string; - sent_at?: string; - delivered_at?: string; - failed_at?: string; - error_message?: string; - created_at: string; -} - -export type NotificationChannel = 'email' | 'whatsapp' | 'push' | 'sms'; -export type NotificationStatus = - | 'pending' - | 'scheduled' - | 'sent' - | 'delivered' - | 'failed' - | 'cancelled'; -export type NotificationPriority = 'low' | 'normal' | 'high' | 'urgent'; - -export interface NotificationTemplate { - id: string; - tenant_id: string; - name: string; - channel: NotificationChannel; - subject_template?: string; - body_template: string; - variables: string[]; - is_system: boolean; - is_active: boolean; - created_at: string; - updated_at?: string; -} - -export interface NotificationHistory { - id: string; - notification_id: string; - status: NotificationStatus; - timestamp: string; - details?: string; - metadata?: Record; -} - -export interface NotificationStats { - total_sent: number; - total_delivered: number; - total_failed: number; - delivery_rate: number; - channels_breakdown: Record; - recent_activity: NotificationResponse[]; -} - -export interface BulkNotificationRequest { - recipients: BulkRecipient[]; - channel: NotificationChannel; - template_id?: string; - subject?: string; - message: string; - scheduled_for?: string; - priority?: NotificationPriority; -} - -export interface BulkRecipient { - recipient_id?: string; - recipient_email?: string; - recipient_phone?: string; - data?: Record; -} - -export interface BulkNotificationStatus { - batch_id: string; - total_recipients: number; - sent_count: number; - failed_count: number; - pending_count: number; - status: 'processing' | 'completed' | 'failed'; - created_at: string; - completed_at?: string; -} \ No newline at end of file diff --git a/fdev-ffrontend/src/api/types/procurement.ts b/fdev-ffrontend/src/api/types/procurement.ts deleted file mode 100644 index b00ffe9e..00000000 --- a/fdev-ffrontend/src/api/types/procurement.ts +++ /dev/null @@ -1,330 +0,0 @@ -// ================================================================ -// frontend/src/api/types/procurement.ts -// ================================================================ -/** - * TypeScript types for procurement planning API - */ - -// ================================================================ -// BASE TYPES -// ================================================================ - -export interface ProcurementRequirement { - id: string; - plan_id: string; - requirement_number: string; - - // Product information - product_id: string; - product_name: string; - product_sku?: string; - product_category?: string; - product_type: string; - - // Quantity requirements - required_quantity: number; - unit_of_measure: string; - safety_stock_quantity: number; - total_quantity_needed: number; - - // Current inventory situation - current_stock_level: number; - reserved_stock: number; - available_stock: number; - net_requirement: number; - - // Demand breakdown - order_demand: number; - production_demand: number; - forecast_demand: number; - buffer_demand: number; - - // Supplier information - preferred_supplier_id?: string; - backup_supplier_id?: string; - supplier_name?: string; - supplier_lead_time_days?: number; - minimum_order_quantity?: number; - - // Pricing - estimated_unit_cost?: number; - estimated_total_cost?: number; - last_purchase_cost?: number; - cost_variance: number; - - // Timing - required_by_date: string; - lead_time_buffer_days: number; - suggested_order_date: string; - latest_order_date: string; - - // Status and priority - status: string; - priority: string; - risk_level: string; - - // Purchase tracking - purchase_order_id?: string; - purchase_order_number?: string; - ordered_quantity: number; - ordered_at?: string; - - // Delivery tracking - expected_delivery_date?: string; - actual_delivery_date?: string; - received_quantity: number; - delivery_status: string; - - // Performance metrics - fulfillment_rate?: number; - on_time_delivery?: boolean; - quality_rating?: number; - - // Approval - approved_quantity?: number; - approved_cost?: number; - approved_at?: string; - approved_by?: string; - - // Additional info - special_requirements?: string; - storage_requirements?: string; - shelf_life_days?: number; - quality_specifications?: Record; - procurement_notes?: string; -} - -export interface ProcurementPlan { - id: string; - tenant_id: string; - plan_number: string; - - // Plan scope and timing - plan_date: string; - plan_period_start: string; - plan_period_end: string; - planning_horizon_days: number; - - // Plan status and lifecycle - status: string; - plan_type: string; - priority: string; - - // Business context - business_model?: string; - procurement_strategy: string; - - // Plan totals and summary - total_requirements: number; - total_estimated_cost: number; - total_approved_cost: number; - cost_variance: number; - - // Demand analysis - total_demand_orders: number; - total_demand_quantity: number; - total_production_requirements: number; - safety_stock_buffer: number; - - // Supplier coordination - primary_suppliers_count: number; - backup_suppliers_count: number; - supplier_diversification_score?: number; - - // Risk assessment - supply_risk_level: string; - demand_forecast_confidence?: number; - seasonality_adjustment: number; - - // Execution tracking - approved_at?: string; - approved_by?: string; - execution_started_at?: string; - execution_completed_at?: string; - - // Performance metrics - fulfillment_rate?: number; - on_time_delivery_rate?: number; - cost_accuracy?: number; - quality_score?: number; - - // Metadata - created_at: string; - updated_at: string; - created_by?: string; - updated_by?: string; - - // Additional info - special_requirements?: string; - - // Relationships - requirements: ProcurementRequirement[]; -} - -// ================================================================ -// REQUEST TYPES -// ================================================================ - -export interface GeneratePlanRequest { - plan_date?: string; - force_regenerate?: boolean; - planning_horizon_days?: number; - include_safety_stock?: boolean; - safety_stock_percentage?: number; -} - -export interface ForecastRequest { - target_date: string; - horizon_days?: number; - include_confidence_intervals?: boolean; - product_ids?: string[]; -} - -// ================================================================ -// RESPONSE TYPES -// ================================================================ - -export interface GeneratePlanResponse { - success: boolean; - message: string; - plan?: ProcurementPlan; - warnings?: string[]; - errors?: string[]; -} - -export interface PaginatedProcurementPlans { - plans: ProcurementPlan[]; - total: number; - page: number; - limit: number; - has_more: boolean; -} - -// ================================================================ -// DASHBOARD TYPES -// ================================================================ - -export interface ProcurementSummary { - total_plans: number; - active_plans: number; - total_requirements: number; - pending_requirements: number; - critical_requirements: number; - - total_estimated_cost: number; - total_approved_cost: number; - cost_variance: number; - - average_fulfillment_rate?: number; - average_on_time_delivery?: number; - - top_suppliers: Array>; - critical_items: Array>; -} - -export interface DashboardData { - current_plan?: ProcurementPlan; - summary: ProcurementSummary; - - upcoming_deliveries: Array>; - overdue_requirements: Array>; - low_stock_alerts: Array>; - - performance_metrics: Record; -} - -// ================================================================ -// FILTER AND SEARCH TYPES -// ================================================================ - -export interface ProcurementFilters { - status?: string[]; - priority?: string[]; - risk_level?: string[]; - supplier_id?: string; - product_category?: string; - date_range?: { - start: string; - end: string; - }; -} - -export interface RequirementFilters { - status?: string[]; - priority?: string[]; - product_type?: string[]; - overdue_only?: boolean; - critical_only?: boolean; -} - -// ================================================================ -// UI COMPONENT TYPES -// ================================================================ - -export interface ProcurementPlanCardProps { - plan: ProcurementPlan; - onViewDetails?: (planId: string) => void; - onUpdateStatus?: (planId: string, status: string) => void; - showActions?: boolean; -} - -export interface RequirementCardProps { - requirement: ProcurementRequirement; - onViewDetails?: (requirementId: string) => void; - onUpdateStatus?: (requirementId: string, status: string) => void; - showSupplierInfo?: boolean; -} - -export interface ProcurementDashboardProps { - showFilters?: boolean; - refreshInterval?: number; - onPlanGenerated?: (plan: ProcurementPlan) => void; -} - -// ================================================================ -// ENUMS -// ================================================================ - -export enum PlanStatus { - DRAFT = 'draft', - PENDING_APPROVAL = 'pending_approval', - APPROVED = 'approved', - IN_EXECUTION = 'in_execution', - COMPLETED = 'completed', - CANCELLED = 'cancelled' -} - -export enum RequirementStatus { - PENDING = 'pending', - APPROVED = 'approved', - ORDERED = 'ordered', - PARTIALLY_RECEIVED = 'partially_received', - RECEIVED = 'received', - CANCELLED = 'cancelled' -} - -export enum Priority { - CRITICAL = 'critical', - HIGH = 'high', - NORMAL = 'normal', - LOW = 'low' -} - -export enum RiskLevel { - LOW = 'low', - MEDIUM = 'medium', - HIGH = 'high', - CRITICAL = 'critical' -} - -export enum PlanType { - REGULAR = 'regular', - EMERGENCY = 'emergency', - SEASONAL = 'seasonal' -} - -export enum ProductType { - INGREDIENT = 'ingredient', - PACKAGING = 'packaging', - SUPPLIES = 'supplies' -} \ No newline at end of file diff --git a/fdev-ffrontend/src/api/types/tenant.ts b/fdev-ffrontend/src/api/types/tenant.ts deleted file mode 100644 index 6103217f..00000000 --- a/fdev-ffrontend/src/api/types/tenant.ts +++ /dev/null @@ -1,143 +0,0 @@ -// frontend/src/api/types/tenant.ts -/** - * Tenant Management Types - */ - -export interface TenantInfo { - id: string; - name: string; - description?: string; - owner_id: string; - is_active: boolean; - created_at: string; - updated_at?: string; - settings?: TenantSettings; - subscription?: TenantSubscription; - location?: TenantLocation; - business_type?: 'bakery' | 'coffee_shop' | 'pastry_shop' | 'restaurant'; - business_model?: 'individual_bakery' | 'central_baker_satellite' | 'retail_bakery' | 'hybrid_bakery'; - // Added properties for compatibility - address?: string; - products?: any[]; -} - -export interface Tenant { - id: string; - name: string; - business_type: string; - address: string; - products: any[]; - created_at: string; - updated_at: string; - owner_id: string; -} - -export interface TenantSettings { - language: string; - timezone: string; - currency: string; - date_format: string; - notification_preferences: Record; - business_hours: BusinessHours; - operating_hours?: BusinessHours; -} - -export interface BusinessHours { - monday: DaySchedule; - tuesday: DaySchedule; - wednesday: DaySchedule; - thursday: DaySchedule; - friday: DaySchedule; - saturday: DaySchedule; - sunday: DaySchedule; -} - -export interface DaySchedule { - open: string; - close: string; - closed: boolean; -} - -export interface TenantLocation { - address: string; - city: string; - country: string; - postal_code: string; - latitude?: number; - longitude?: number; -} - -export interface TenantSubscription { - plan: string; - status: string; - billing_cycle: string; - current_period_start: string; - current_period_end: string; - cancel_at_period_end: boolean; -} - -export interface TenantCreate { - name: string; - address?: string; - business_type?: 'bakery' | 'coffee_shop' | 'pastry_shop' | 'restaurant'; - business_model?: 'individual_bakery' | 'central_baker_satellite' | 'retail_bakery' | 'hybrid_bakery'; - postal_code: string; - phone: string; - description?: string; - settings?: Partial; - location?: TenantLocation; - coordinates?: { lat: number; lng: number }; - products?: string[]; - has_historical_data?: boolean; -} - -export interface TenantUpdate { - name?: string; - description?: string; - settings?: Partial; - location?: TenantLocation; -} - -export interface TenantMember { - user_id: string; - tenant_id: string; - role: 'owner' | 'admin' | 'member' | 'viewer'; - is_active: boolean; - joined_at: string; - user: { - id: string; - email: string; - full_name: string; - }; - // Additional properties for compatibility - id?: string; - status?: 'active' | 'inactive' | 'pending'; - last_active?: string; -} - -export interface UserMember { - id: string; - email: string; - full_name: string; - role: 'owner' | 'admin' | 'member' | 'viewer'; - status: 'active' | 'inactive' | 'pending'; - joined_at: string; - last_active?: string; -} - -export interface InviteUser { - email: string; - role: 'admin' | 'member' | 'viewer'; - message?: string; -} - -export interface TenantStats { - tenant_id: string; - total_members: number; - active_members: number; - total_predictions: number; - models_trained: number; - last_training_date?: string; - subscription_plan: string; - subscription_status: string; -} diff --git a/fdev-ffrontend/src/api/types/training.ts b/fdev-ffrontend/src/api/types/training.ts deleted file mode 100644 index 49c566c1..00000000 --- a/fdev-ffrontend/src/api/types/training.ts +++ /dev/null @@ -1,132 +0,0 @@ -// frontend/src/api/types/training.ts -/** - * Training Service Types - */ - -export interface TrainingJobRequest { - config?: TrainingJobConfig; - priority?: number; - schedule_time?: string; - include_weather?: boolean; - include_traffic?: boolean; - min_data_points?: number; - use_default_data?: boolean; -} - -export interface SingleProductTrainingRequest { - inventory_product_id: string; - config?: TrainingJobConfig; - priority?: number; -} - -export interface TrainingJobConfig { - external_data?: ExternalDataConfig; - prophet_params?: Record; - data_filters?: Record; - validation_params?: Record; -} - -export interface ExternalDataConfig { - weather_enabled: boolean; - traffic_enabled: boolean; - weather_features: string[]; - traffic_features: string[]; -} - -export interface TrainingJobResponse { - job_id: string; - tenant_id: string; - status: TrainingJobStatus; - config: TrainingJobConfig; - priority: number; - created_at: string; - started_at?: string; - completed_at?: string; - error_message?: string; - progress?: TrainingJobProgress; - results?: TrainingJobResults; -} - -export type TrainingJobStatus = - | 'pending' - | 'running' - | 'completed' - | 'failed' - | 'cancelled'; - -export interface TrainingJobProgress { - current_step: string; - total_steps: number; - completed_steps: number; - percentage: number; - current_product?: string; - total_products?: number; - completed_products?: number; - estimated_completion?: string; - detailed_progress?: StepProgress[]; -} - -export interface StepProgress { - step_name: string; - status: 'pending' | 'running' | 'completed' | 'failed'; - progress_percentage: number; - start_time?: string; - end_time?: string; - duration_seconds?: number; -} - -export interface TrainingJobResults { - models_trained: number; - models_failed: number; - total_training_time_seconds: number; - average_model_accuracy?: number; - trained_models: TrainedModelInfo[]; - failed_products?: string[]; // inventory_product_ids of failed products -} - -export interface TrainedModelInfo { - inventory_product_id: string; - product_name?: string; // Optional - for display, populated by frontend from inventory service - model_id: string; - model_type: string; - accuracy_metrics: TrainingMetrics; - training_time_seconds: number; - data_points_used: number; - model_path: string; -} - -export interface TrainingMetrics { - mae: number; - mse: number; - rmse: number; - mape: number; - r2_score: number; - mean_actual: number; - mean_predicted: number; -} - -export interface ModelInfo { - model_id: string; - tenant_id: string; - inventory_product_id: string; - product_name?: string; // Optional - for display, populated by frontend from inventory service - model_type: string; - model_path: string; - version: number; - training_samples: number; - features: string[]; - hyperparameters: Record; - training_metrics: Record; - is_active: boolean; - created_at: string; - data_period_start?: string; - data_period_end?: string; -} - -export interface ModelTrainingStats { - total_models: number; - active_models: number; - last_training_date?: string; - avg_training_time_minutes: number; - success_rate: number; -} \ No newline at end of file diff --git a/fdev-ffrontend/src/api/utils/error.ts b/fdev-ffrontend/src/api/utils/error.ts deleted file mode 100644 index cf576ed7..00000000 --- a/fdev-ffrontend/src/api/utils/error.ts +++ /dev/null @@ -1,50 +0,0 @@ -// frontend/src/api/utils/error.ts -/** - * Error Handling Utilities - */ - -import type { ApiError } from '../client/types'; - -export class ApiErrorHandler { - static formatError(error: any): string { - if (error?.response?.data) { - const errorData = error.response.data as ApiError; - return errorData.detail || errorData.message || 'An error occurred'; - } - - if (error?.message) { - return error.message; - } - - return 'An unexpected error occurred'; - } - - static getErrorCode(error: any): string | undefined { - return error?.response?.data?.error_code; - } - - static isNetworkError(error: any): boolean { - return !error?.response && error?.message?.includes('Network'); - } - - static isAuthError(error: any): boolean { - const status = error?.response?.status; - return status === 401 || status === 403; - } - - static isValidationError(error: any): boolean { - return error?.response?.status === 422; - } - - static isServerError(error: any): boolean { - const status = error?.response?.status; - return status >= 500; - } - - static shouldRetry(error: any): boolean { - if (this.isNetworkError(error)) return true; - if (this.isServerError(error)) return true; - const status = error?.response?.status; - return status === 408 || status === 429; // Timeout or Rate limited - } -} \ No newline at end of file diff --git a/fdev-ffrontend/src/api/utils/index.ts b/fdev-ffrontend/src/api/utils/index.ts deleted file mode 100644 index 7a8945f5..00000000 --- a/fdev-ffrontend/src/api/utils/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -// frontend/src/api/utils/index.ts -/** - * Main Utils Export - */ - -export { ApiErrorHandler } from './error'; -export { ResponseProcessor } from './response'; -export { RequestValidator } from './validation'; -export { DataTransformer } from './transform'; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/utils/response.ts b/fdev-ffrontend/src/api/utils/response.ts deleted file mode 100644 index 39805b5c..00000000 --- a/fdev-ffrontend/src/api/utils/response.ts +++ /dev/null @@ -1,34 +0,0 @@ -// frontend/src/api/utils/response.ts -/** - * Response Processing Utilities - */ - -import type { ApiResponse, PaginatedResponse } from '../types'; - -export class ResponseProcessor { - static extractData(response: ApiResponse): T { - return response.data; - } - - static extractPaginatedData(response: PaginatedResponse): { - data: T[]; - pagination: PaginatedResponse['pagination']; - } { - return { - data: response.data, - pagination: response.pagination, - }; - } - - static isSuccessResponse(response: ApiResponse): boolean { - return response.status === 'success' || response.status === 'ok'; - } - - static extractMessage(response: ApiResponse): string | undefined { - return response.message; - } - - static extractMeta(response: ApiResponse): any { - return response.meta; - } -} diff --git a/fdev-ffrontend/src/api/utils/transform.ts b/fdev-ffrontend/src/api/utils/transform.ts deleted file mode 100644 index fc682823..00000000 --- a/fdev-ffrontend/src/api/utils/transform.ts +++ /dev/null @@ -1,70 +0,0 @@ -// frontend/src/api/utils/transform.ts -/** - * Data Transformation Utilities - */ - -export class DataTransformer { - static formatDate(date: string | Date): string { - const d = new Date(date); - return d.toLocaleDateString(); - } - - static formatDateTime(date: string | Date): string { - const d = new Date(date); - return d.toLocaleString(); - } - - static formatCurrency(amount: number, currency = 'EUR'): string { - return new Intl.NumberFormat('es-ES', { - style: 'currency', - currency, - }).format(amount); - } - - static formatPercentage(value: number, decimals = 1): string { - return `${(value * 100).toFixed(decimals)}%`; - } - - static formatFileSize(bytes: number): string { - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - if (bytes === 0) return '0 Bytes'; - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - return `${Math.round(bytes / Math.pow(1024, i) * 100) / 100} ${sizes[i]}`; - } - - static slugify(text: string): string { - return text - .toLowerCase() - .replace(/[^\w ]+/g, '') - .replace(/ +/g, '-'); - } - - static truncate(text: string, length: number): string { - if (text.length <= length) return text; - return `${text.substring(0, length)}...`; - } - - static camelToKebab(str: string): string { - return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase(); - } - - static kebabToCamel(str: string): string { - return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); - } - - static deepClone(obj: T): T { - return JSON.parse(JSON.stringify(obj)); - } - - static removeEmpty(obj: Record): Record { - const cleaned: Record = {}; - - Object.keys(obj).forEach(key => { - if (obj[key] !== null && obj[key] !== undefined && obj[key] !== '') { - cleaned[key] = obj[key]; - } - }); - - return cleaned; - } -} diff --git a/fdev-ffrontend/src/api/utils/validation.ts b/fdev-ffrontend/src/api/utils/validation.ts deleted file mode 100644 index 3c252bc6..00000000 --- a/fdev-ffrontend/src/api/utils/validation.ts +++ /dev/null @@ -1,72 +0,0 @@ -// frontend/src/api/utils/validation.ts -/** - * Request Validation Utilities - */ - -export class RequestValidator { - static validateEmail(email: string): boolean { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); - } - - static validatePassword(password: string): { - valid: boolean; - errors: string[]; - } { - const errors: string[] = []; - - if (password.length < 8) { - errors.push('Password must be at least 8 characters long'); - } - - if (!/(?=.*[a-z])/.test(password)) { - errors.push('Password must contain at least one lowercase letter'); - } - - if (!/(?=.*[A-Z])/.test(password)) { - errors.push('Password must contain at least one uppercase letter'); - } - - if (!/(?=.*\d)/.test(password)) { - errors.push('Password must contain at least one number'); - } - - return { - valid: errors.length === 0, - errors, - }; - } - - static validateFile(file: File, allowedTypes: string[], maxSize: number): { - valid: boolean; - errors: string[]; - } { - const errors: string[] = []; - - if (!allowedTypes.includes(file.type)) { - errors.push(`File type ${file.type} is not allowed`); - } - - if (file.size > maxSize) { - errors.push(`File size exceeds maximum of ${maxSize} bytes`); - } - - return { - valid: errors.length === 0, - errors, - }; - } - - static validatePhoneNumber(phone: string): boolean { - // Basic international phone number validation - const phoneRegex = /^\+?[1-9]\d{1,14}$/; - return phoneRegex.test(phone.replace(/\s/g, '')); - } - - static validateRequired(value: any, fieldName: string): string | null { - if (value === null || value === undefined || value === '') { - return `${fieldName} is required`; - } - return null; - } -} diff --git a/fdev-ffrontend/src/api/websocket/hooks.ts b/fdev-ffrontend/src/api/websocket/hooks.ts deleted file mode 100644 index 5fd80575..00000000 --- a/fdev-ffrontend/src/api/websocket/hooks.ts +++ /dev/null @@ -1,433 +0,0 @@ -// frontend/src/api/websocket/hooks.ts -/** - * WebSocket React Hooks - */ - -import { useState, useEffect, useCallback, useRef } from 'react'; -import { WebSocketManager } from './manager'; -import type { - WebSocketConfig, - WebSocketMessage, - WebSocketHandlers, - WebSocketStatus, -} from './types'; - -export const useWebSocket = (config: WebSocketConfig) => { - const [status, setStatus] = useState('disconnected'); - const [lastMessage, setLastMessage] = useState(null); - const [error, setError] = useState(null); - const wsManagerRef = useRef(null); - - // Initialize WebSocket manager - useEffect(() => { - wsManagerRef.current = new WebSocketManager(config); - - const handlers: WebSocketHandlers = { - onOpen: () => { - setStatus('connected'); - setError(null); - }, - onMessage: (message) => { - setLastMessage(message); - }, - onError: (error) => { - setError('WebSocket connection error'); - setStatus('failed'); - }, - onClose: () => { - setStatus('disconnected'); - }, - onReconnect: () => { - setStatus('reconnecting'); - setError(null); - }, - onReconnectFailed: () => { - setStatus('failed'); - setError('Failed to reconnect'); - }, - }; - - wsManagerRef.current.setHandlers(handlers); - - return () => { - wsManagerRef.current?.disconnect(); - }; - }, [config.url]); - - const connect = useCallback(async () => { - try { - setError(null); - await wsManagerRef.current?.connect(); - } catch (error) { - setError(error instanceof Error ? error.message : 'Connection failed'); - } - }, []); - - const disconnect = useCallback(() => { - wsManagerRef.current?.disconnect(); - }, []); - - const sendMessage = useCallback((message: Omit) => { - return wsManagerRef.current?.send(message) ?? false; - }, []); - - const addMessageHandler = useCallback((handler: (message: WebSocketMessage) => void) => { - const currentHandlers = wsManagerRef.current?.['handlers'] || {}; - wsManagerRef.current?.setHandlers({ - ...currentHandlers, - onMessage: (message) => { - setLastMessage(message); - handler(message); - }, - }); - }, []); - - return { - status, - lastMessage, - error, - connect, - disconnect, - sendMessage, - addMessageHandler, - isConnected: status === 'connected', - }; -}; - -// Hook for training job updates -export const useTrainingWebSocket = (jobId: string, tenantId?: string) => { - const [jobUpdates, setJobUpdates] = useState([]); - const [connectionError, setConnectionError] = useState(null); - const [isAuthenticationError, setIsAuthenticationError] = useState(false); - - // Get tenant ID reliably with enhanced error handling - const actualTenantId = tenantId || (() => { - try { - // Try multiple sources for tenant ID - const sources = [ - () => localStorage.getItem('current_tenant_id'), - () => { - const userData = localStorage.getItem('user_data'); - if (userData) { - const parsed = JSON.parse(userData); - return parsed.current_tenant_id || parsed.tenant_id; - } - return null; - }, - () => { - const authData = localStorage.getItem('auth_data'); - if (authData) { - const parsed = JSON.parse(authData); - return parsed.tenant_id; - } - return null; - }, - () => { - const tenantContext = localStorage.getItem('tenant_context'); - if (tenantContext) { - const parsed = JSON.parse(tenantContext); - return parsed.current_tenant_id; - } - return null; - } - ]; - - for (const source of sources) { - try { - const tenantId = source(); - if (tenantId) return tenantId; - } catch (e) { - console.warn('Failed to get tenant ID from source:', e); - } - } - } catch (e) { - console.error('Failed to parse tenant ID from storage:', e); - } - return null; - })(); - - const config = { - url: actualTenantId - ? `ws://localhost:8000/api/v1/ws/tenants/${actualTenantId}/training/jobs/${jobId}/live` - : `ws://localhost:8000/api/v1/ws/tenants/unknown/training/jobs/${jobId}/live`, - reconnect: true, - reconnectInterval: 3000, // Faster reconnection for training - maxReconnectAttempts: 20, // More attempts for long training jobs - heartbeatInterval: 15000, // Send heartbeat every 15 seconds for training jobs - enableLogging: true // Enable logging for debugging - }; - - const { - status, - connect, - disconnect, - addMessageHandler, - isConnected, - lastMessage, - sendMessage - } = useWebSocket(config); - - // Enhanced message handler with error handling - const handleWebSocketMessage = useCallback((message: any) => { - try { - // Clear connection error when receiving messages - setConnectionError(null); - setIsAuthenticationError(false); - - // Handle different message structures - let processedMessage = message; - - // If message has nested data, flatten it for easier processing - if (message.data && typeof message.data === 'object') { - processedMessage = { - ...message, - // Merge data properties to root level for backward compatibility - ...message.data, - // Preserve original structure - _originalData: message.data - }; - } - - // Handle special message types - if (message.type === 'connection_established') { - console.log('WebSocket training connection established:', message); - setJobUpdates(prev => [{ - type: 'connection_established', - message: 'Connected to training service', - timestamp: Date.now() - }, ...prev.slice(0, 49)]); - return; - } - - // Handle keepalive messages (don't show to user, just for connection health) - if (message.type === 'pong' || message.type === 'heartbeat') { - console.debug('Training WebSocket keepalive received:', message.type); - return; // Don't add to jobUpdates - } - - if (message.type === 'authentication_error' || message.type === 'authorization_error') { - console.error('WebSocket auth/authorization error:', message); - setIsAuthenticationError(true); - setConnectionError(message.message || 'Authentication/authorization failed - please refresh and try again'); - return; - } - - if (message.type === 'connection_error') { - console.error('WebSocket connection error:', message); - setConnectionError(message.message || 'Connection error'); - return; - } - - if (message.type === 'connection_timeout') { - console.warn('WebSocket connection timeout:', message); - // Don't set as error, just log - connection will retry - return; - } - - if (message.type === 'job_not_found') { - console.error('Training job not found:', message); - setConnectionError('Training job not found. Please restart the training process.'); - return; - } - - // Comprehensive message type handling - const trainingMessageTypes = [ - 'progress', 'training_progress', - 'completed', 'training_completed', - 'failed', 'training_failed', - 'error', 'training_error', - 'started', 'training_started', - 'heartbeat', 'initial_status', - 'status_update' - ]; - - if (trainingMessageTypes.includes(message.type)) { - // Add to updates array with processed message - setJobUpdates(prev => { - const newUpdates = [processedMessage, ...prev.slice(0, 49)]; // Keep last 50 messages - return newUpdates; - }); - } else { - // Still add to updates for debugging purposes - console.log('Received unknown message type:', message.type, message); - setJobUpdates(prev => [processedMessage, ...prev.slice(0, 49)]); - } - } catch (error) { - console.error('Error processing WebSocket message:', error, message); - } - }, []); - - // Set up message handler when hook initializes - useEffect(() => { - addMessageHandler(handleWebSocketMessage); - }, [addMessageHandler, handleWebSocketMessage]); - - // Enhanced dual ping system for training jobs - prevent disconnection during long training - useEffect(() => { - if (isConnected) { - // Primary ping system using JSON messages with training info - const keepaliveInterval = setInterval(() => { - const success = sendMessage({ - type: 'training_keepalive', - data: { - timestamp: Date.now(), - job_id: jobId, - tenant_id: actualTenantId, - status: 'active' - } - }); - - if (!success) { - console.warn('Training keepalive failed - connection may be lost'); - } - }, 10000); // Every 10 seconds for training jobs - - // Secondary simple text ping system (more lightweight) - const simplePingInterval = setInterval(() => { - // Send a simple text ping to keep connection alive - const success = sendMessage({ - type: 'ping', - data: { - timestamp: Date.now(), - source: 'training_client' - } - }); - - if (!success) { - console.warn('Simple training ping failed'); - } - }, 15000); // Every 15 seconds - - return () => { - clearInterval(keepaliveInterval); - clearInterval(simplePingInterval); - }; - } - }, [isConnected, sendMessage, jobId, actualTenantId]); - - // Define refresh connection function - const refreshConnection = useCallback(() => { - setConnectionError(null); - setIsAuthenticationError(false); - disconnect(); - setTimeout(() => { - connect(); - }, 1000); - }, [connect, disconnect]); - - // Enhanced connection monitoring and auto-recovery for training jobs - useEffect(() => { - if (actualTenantId && jobId !== 'pending') { - const healthCheckInterval = setInterval(() => { - // If we should be connected but aren't, try to reconnect - if (status === 'disconnected' && !connectionError) { - console.log('WebSocket health check: reconnecting disconnected training socket'); - connect(); - } - - // More aggressive stale connection detection for training jobs - const lastUpdate = jobUpdates.length > 0 ? jobUpdates[0] : null; - if (lastUpdate && status === 'connected') { - const timeSinceLastMessage = Date.now() - (lastUpdate.timestamp || 0); - if (timeSinceLastMessage > 45000) { // 45 seconds without messages during training - console.log('WebSocket health check: connection appears stale, refreshing'); - refreshConnection(); - } - } - - // If connection is in a failed state for too long, force reconnect - if (status === 'failed' && !isAuthenticationError) { - console.log('WebSocket health check: recovering from failed state'); - setTimeout(() => connect(), 2000); - } - }, 12000); // Check every 12 seconds for training jobs - - return () => clearInterval(healthCheckInterval); - } - }, [actualTenantId, jobId, status, connectionError, connect, refreshConnection, jobUpdates, isAuthenticationError]); - - // Enhanced connection setup - request current status when connecting - useEffect(() => { - if (isConnected && jobId !== 'pending') { - // Wait a moment for connection to stabilize, then request current status - const statusRequestTimer = setTimeout(() => { - console.log('Requesting current training status after connection'); - sendMessage({ - type: 'get_status', - data: { - job_id: jobId, - tenant_id: actualTenantId - } - }); - }, 2000); - - return () => clearTimeout(statusRequestTimer); - } - }, [isConnected, jobId, actualTenantId, sendMessage]); - - return { - status, - jobUpdates, - connect, - disconnect, - isConnected, - lastMessage, - tenantId: actualTenantId, - wsUrl: config.url, - connectionError, - isAuthenticationError, - // Enhanced refresh function with status request - refreshConnection, - // Force retry with new authentication - retryWithAuth: useCallback(() => { - setConnectionError(null); - setIsAuthenticationError(false); - // Clear any cached auth data that might be stale - disconnect(); - setTimeout(() => { - connect(); - }, 2000); - }, [connect, disconnect]), - // Manual status request function - requestStatus: useCallback(() => { - if (isConnected && jobId !== 'pending') { - return sendMessage({ - type: 'get_status', - data: { - job_id: jobId, - tenant_id: actualTenantId - } - }); - } - return false; - }, [isConnected, jobId, actualTenantId, sendMessage]) - }; -}; - -// Hook for forecast alerts -export const useForecastWebSocket = (tenantId: string) => { - const config: WebSocketConfig = { - url: `ws://localhost:8000/api/v1/ws/forecasts/${tenantId}`, - reconnect: true, - }; - - const [alerts, setAlerts] = useState([]); - - const { status, connect, disconnect, addMessageHandler, isConnected } = useWebSocket(config); - - useEffect(() => { - addMessageHandler((message) => { - if (message.type === 'forecast_alert') { - setAlerts(prev => [message.data, ...prev]); - } - }); - }, [addMessageHandler]); - - return { - status, - alerts, - connect, - disconnect, - isConnected, - }; -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/websocket/index.ts b/fdev-ffrontend/src/api/websocket/index.ts deleted file mode 100644 index b351bfd3..00000000 --- a/fdev-ffrontend/src/api/websocket/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// frontend/src/api/websocket/index.ts -/** - * Main WebSocket Export - */ - -export { WebSocketManager } from './manager'; -export { - useWebSocket, - useTrainingWebSocket, - useForecastWebSocket, -} from './hooks'; -export type { - WebSocketConfig, - WebSocketMessage, - WebSocketHandlers, - WebSocketStatus, - WebSocketMetrics, -} from './types'; \ No newline at end of file diff --git a/fdev-ffrontend/src/api/websocket/manager.ts b/fdev-ffrontend/src/api/websocket/manager.ts deleted file mode 100644 index e1869da9..00000000 --- a/fdev-ffrontend/src/api/websocket/manager.ts +++ /dev/null @@ -1,274 +0,0 @@ -// frontend/src/api/websocket/manager.ts -/** - * WebSocket Manager - * Handles WebSocket connections with auto-reconnection and heartbeat - */ - -import { apiConfig } from '../client/config'; -import type { - WebSocketConfig, - WebSocketMessage, - WebSocketHandlers, - WebSocketStatus, - WebSocketMetrics, -} from './types'; - -export class WebSocketManager { - private ws: WebSocket | null = null; - private config: WebSocketConfig; - private handlers: WebSocketHandlers = {}; - private status: WebSocketStatus = 'disconnected'; - private reconnectTimer: NodeJS.Timeout | null = null; - private heartbeatTimer: NodeJS.Timeout | null = null; - private reconnectAttempts = 0; - private metrics: WebSocketMetrics = { - reconnectAttempts: 0, - messagesReceived: 0, - messagesSent: 0, - lastActivity: new Date(), - }; - - constructor(config: WebSocketConfig) { - this.config = { - reconnect: true, - reconnectInterval: 5000, - maxReconnectAttempts: 10, - heartbeatInterval: 30000, - enableLogging: apiConfig.enableLogging, - ...config, - }; - } - - /** - * Connect to WebSocket - */ - connect(): Promise { - return new Promise((resolve, reject) => { - try { - this.status = 'connecting'; - this.log('Connecting to WebSocket:', this.config.url); - - // Add authentication token to URL if available - const token = localStorage.getItem('auth_token'); - const wsUrl = token - ? `${this.config.url}?token=${encodeURIComponent(token)}` - : this.config.url; - - this.ws = new WebSocket(wsUrl, this.config.protocols); - - this.ws.onopen = (event) => { - this.status = 'connected'; - this.reconnectAttempts = 0; - this.metrics.connectionTime = Date.now(); - this.metrics.lastActivity = new Date(); - - this.log('WebSocket connected'); - this.startHeartbeat(); - - this.handlers.onOpen?.(event); - resolve(); - }; - - this.ws.onmessage = (event) => { - this.metrics.messagesReceived++; - this.metrics.lastActivity = new Date(); - - try { - const message: WebSocketMessage = JSON.parse(event.data); - this.log('WebSocket message received:', message.type); - this.handlers.onMessage?.(message); - } catch (error) { - this.log('Failed to parse WebSocket message:', error); - } - }; - - this.ws.onerror = (error) => { - this.log('WebSocket error:', error); - this.handlers.onError?.(error); - - if (this.status === 'connecting') { - reject(new Error('WebSocket connection failed')); - } - }; - - this.ws.onclose = (event) => { - this.log('WebSocket closed:', event.code, event.reason); - this.status = 'disconnected'; - this.stopHeartbeat(); - - this.handlers.onClose?.(event); - - // Auto-reconnect if enabled and not manually closed - // Don't reconnect on authorization failures or job not found (1008) with specific reasons - const isAuthorizationError = event.code === 1008 && - (event.reason === 'Authentication failed' || event.reason === 'Authorization failed'); - const isJobNotFound = event.code === 1008 && event.reason === 'Job not found'; - - if (this.config.reconnect && event.code !== 1000 && !isAuthorizationError && !isJobNotFound) { - this.scheduleReconnect(); - } else if (isAuthorizationError || isJobNotFound) { - this.log('Connection failed - stopping reconnection attempts:', event.reason); - this.status = 'failed'; - this.handlers.onReconnectFailed?.(); - } - }; - - } catch (error) { - this.status = 'failed'; - reject(error); - } - }); - } - - /** - * Disconnect from WebSocket - */ - disconnect(): void { - this.config.reconnect = false; // Disable auto-reconnect - this.clearReconnectTimer(); - this.stopHeartbeat(); - - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - this.ws.close(1000, 'Manual disconnect'); - } - - this.ws = null; - this.status = 'disconnected'; - } - - /** - * Send message through WebSocket - */ - send(message: Omit): boolean { - if (!this.isConnected()) { - this.log('Cannot send message: WebSocket not connected'); - return false; - } - - try { - const fullMessage: WebSocketMessage = { - ...message, - timestamp: new Date().toISOString(), - id: this.generateMessageId(), - }; - - this.ws!.send(JSON.stringify(fullMessage)); - this.metrics.messagesSent++; - this.metrics.lastActivity = new Date(); - - this.log('WebSocket message sent:', message.type); - return true; - } catch (error) { - this.log('Failed to send WebSocket message:', error); - return false; - } - } - - /** - * Set event handlers - */ - setHandlers(handlers: WebSocketHandlers): void { - this.handlers = { ...this.handlers, ...handlers }; - } - - /** - * Get connection status - */ - getStatus(): WebSocketStatus { - return this.status; - } - - /** - * Check if connected - */ - isConnected(): boolean { - return this.ws?.readyState === WebSocket.OPEN; - } - - /** - * Get connection metrics - */ - getMetrics(): WebSocketMetrics { - return { ...this.metrics }; - } - - /** - * Schedule reconnection attempt - */ - private scheduleReconnect(): void { - if (this.reconnectAttempts >= this.config.maxReconnectAttempts!) { - this.status = 'failed'; - this.log('Max reconnection attempts reached'); - this.handlers.onReconnectFailed?.(); - return; - } - - this.status = 'reconnecting'; - this.reconnectAttempts++; - this.metrics.reconnectAttempts++; - - this.log(`Scheduling reconnection attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts}`); - - this.reconnectTimer = setTimeout(async () => { - try { - this.handlers.onReconnect?.(); - await this.connect(); - } catch (error) { - this.log('Reconnection failed:', error); - this.scheduleReconnect(); - } - }, this.config.reconnectInterval); - } - - /** - * Clear reconnection timer - */ - private clearReconnectTimer(): void { - if (this.reconnectTimer) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - } - - /** - * Start heartbeat mechanism - */ - private startHeartbeat(): void { - if (!this.config.heartbeatInterval) return; - - this.heartbeatTimer = setInterval(() => { - if (this.isConnected()) { - this.send({ - type: 'ping', - data: { timestamp: Date.now() }, - }); - } - }, this.config.heartbeatInterval); - } - - /** - * Stop heartbeat mechanism - */ - private stopHeartbeat(): void { - if (this.heartbeatTimer) { - clearInterval(this.heartbeatTimer); - this.heartbeatTimer = null; - } - } - - /** - * Generate unique message ID - */ - private generateMessageId(): string { - return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - } - - /** - * Log message if logging enabled - */ - private log(...args: any[]): void { - if (this.config.enableLogging) { - console.log('[WebSocket]', ...args); - } - } -} diff --git a/fdev-ffrontend/src/api/websocket/types.ts b/fdev-ffrontend/src/api/websocket/types.ts deleted file mode 100644 index fe536c39..00000000 --- a/fdev-ffrontend/src/api/websocket/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -// frontend/src/api/websocket/types.ts -/** - * WebSocket Types - */ - -export interface WebSocketConfig { - url: string; - protocols?: string[]; - reconnect?: boolean; - reconnectInterval?: number; - maxReconnectAttempts?: number; - heartbeatInterval?: number; - enableLogging?: boolean; -} - -export interface WebSocketMessage { - type: string; - data: any; - timestamp: string; - id?: string; -} - -export interface WebSocketHandlers { - onOpen?: (event: Event) => void; - onMessage?: (message: WebSocketMessage) => void; - onError?: (error: Event) => void; - onClose?: (event: CloseEvent) => void; - onReconnect?: () => void; - onReconnectFailed?: () => void; -} - -export type WebSocketStatus = - | 'connecting' - | 'connected' - | 'disconnected' - | 'reconnecting' - | 'failed'; - -export interface WebSocketMetrics { - connectionTime?: number; - reconnectAttempts: number; - messagesReceived: number; - messagesSent: number; - lastActivity: Date; -} \ No newline at end of file diff --git a/fdev-ffrontend/src/components/EnhancedTrainingProgress.tsx b/fdev-ffrontend/src/components/EnhancedTrainingProgress.tsx deleted file mode 100644 index bdad1a8d..00000000 --- a/fdev-ffrontend/src/components/EnhancedTrainingProgress.tsx +++ /dev/null @@ -1,387 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { - Brain, Cpu, Database, TrendingUp, CheckCircle, AlertCircle, - Clock, Zap, Target, BarChart3, Loader -} from 'lucide-react'; - -interface TrainingProgressProps { - progress: { - progress: number; - status: string; - currentStep: string; - productsCompleted: number; - productsTotal: number; - estimatedTimeRemaining: number; - error?: string; - }; - onTimeout?: () => void; -} - -// Map backend steps to user-friendly information -const STEP_INFO_MAP = { - 'data_validation': { - title: 'Validando tus datos', - description: 'Verificamos la calidad y completitud de tu información histórica', - tip: '💡 Datos más completos = predicciones más precisas', - icon: Database, - color: 'blue' - }, - 'feature_engineering': { - title: 'Creando características predictivas', - description: 'Identificamos patrones estacionales y tendencias en tus ventas', - tip: '💡 Tu modelo detectará automáticamente picos de demanda', - icon: TrendingUp, - color: 'indigo' - }, - 'model_training': { - title: 'Entrenando modelo de IA', - description: 'Creamos tu modelo personalizado usando algoritmos avanzados', - tip: '💡 Este proceso optimiza las predicciones para tu negocio específico', - icon: Brain, - color: 'purple' - }, - 'model_validation': { - title: 'Validando precisión', - description: 'Verificamos que el modelo genere predicciones confiables', - tip: '💡 Garantizamos que las predicciones sean útiles para tu toma de decisiones', - icon: Target, - color: 'green' - }, - // Handle any unmapped steps - 'default': { - title: 'Procesando...', - description: 'Procesando tus datos para crear el modelo de predicción', - tip: '💡 Cada paso nos acerca a predicciones más precisas', - icon: Cpu, - color: 'gray' - } -}; - -const EXPECTED_BENEFITS = [ - { - icon: BarChart3, - title: 'Predicciones Precisas', - description: 'Conoce exactamente cuánto vender cada día' - }, - { - icon: Zap, - title: 'Optimización Automática', - description: 'Reduce desperdicios y maximiza ganancias' - }, - { - icon: TrendingUp, - title: 'Detección de Tendencias', - description: 'Identifica patrones estacionales y eventos especiales' - } -]; - -export default function EnhancedTrainingProgress({ progress, onTimeout }: TrainingProgressProps) { - const [showTimeoutWarning, setShowTimeoutWarning] = useState(false); - const [startTime] = useState(Date.now()); - - // Auto-show timeout warning after 8 minutes (480,000ms) - useEffect(() => { - const timeoutTimer = setTimeout(() => { - if (progress.status === 'running' && progress.progress < 100) { - setShowTimeoutWarning(true); - } - }, 480000); // 8 minutes - - return () => clearTimeout(timeoutTimer); - }, [progress.status, progress.progress]); - - const getCurrentStepInfo = () => { - // Try to match the current step from backend - const stepKey = progress.currentStep?.toLowerCase().replace(/\s+/g, '_'); - return STEP_INFO_MAP[stepKey] || STEP_INFO_MAP['default']; - }; - - const formatTime = (seconds: number): string => { - if (!seconds || seconds <= 0) return '0m 0s'; - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return `${minutes}m ${remainingSeconds}s`; - }; - - const getProgressSteps = () => { - // Create progress steps based on current progress percentage - const steps = [ - { id: 'data_validation', threshold: 25, name: 'Validación' }, - { id: 'feature_engineering', threshold: 45, name: 'Características' }, - { id: 'model_training', threshold: 85, name: 'Entrenamiento' }, - { id: 'model_validation', threshold: 100, name: 'Validación' } - ]; - - return steps.map(step => ({ - ...step, - completed: progress.progress >= step.threshold, - current: progress.progress >= (step.threshold - 25) && progress.progress < step.threshold - })); - }; - - const handleContinueToDashboard = () => { - setShowTimeoutWarning(false); - if (onTimeout) { - onTimeout(); - } - }; - - const handleKeepWaiting = () => { - setShowTimeoutWarning(false); - }; - - const currentStepInfo = getCurrentStepInfo(); - const progressSteps = getProgressSteps(); - - // Handle error state - if (progress.status === 'failed' || progress.error) { - return ( -
-
-
- -
-

- Error en el Entrenamiento -

-

- Ha ocurrido un problema durante el entrenamiento. Nuestro equipo ha sido notificado. -

-
- -
-
-
- -
-

- Detalles del Error -

-

- {progress.error || 'Error desconocido durante el entrenamiento'} -

-
-

• Puedes intentar el entrenamiento nuevamente

-

• Verifica que tus datos históricos estén completos

-

• Contacta soporte si el problema persiste

-
-
-
-
- -
- -
-
-
- ); - } - - return ( -
- {/* Header */} -
-
- -
-

- 🧠 Entrenando tu modelo de predicción -

-

- Estamos procesando tus datos históricos para crear predicciones personalizadas -

-
- - {/* Main Progress Section */} -
- {/* Overall Progress Bar */} -
-
- Progreso General - {progress.progress}% -
-
-
-
-
-
-
-
-
- - {/* Current Step Info */} -
-
-
-
- -
-
-
-

- {currentStepInfo.title} -

-

- {currentStepInfo.description} -

-
-

- {currentStepInfo.tip} -

-
-
-
-
- - {/* Step Progress Indicators */} -
- {progressSteps.map((step, index) => ( -
-
- {step.completed ? ( - - ) : step.current ? ( -
- ) : ( -
- )} - - {step.name} - -
-
- ))} -
- - {/* Enhanced Stats Grid */} -
-
-
- - Productos Procesados -
-
- {progress.productsCompleted}/{progress.productsTotal || 'N/A'} -
- {progress.productsTotal > 0 && ( -
-
-
- )} -
- -
-
- - Tiempo Restante -
-
- {progress.estimatedTimeRemaining - ? formatTime(progress.estimatedTimeRemaining * 60) // Convert minutes to seconds - : 'Calculando...' - } -
-
- -
-
- - Precisión Esperada -
-
- ~85% -
-
-
- - {/* Status Indicator */} -
-
- - Estado: {progress.status === 'running' ? 'Entrenando' : progress.status} - - Paso actual: {progress.currentStep || 'Procesando...'} -
-
-
- - {/* Expected Benefits - Only show if progress < 80% to keep user engaged */} - {progress.progress < 80 && ( -
-

- Lo que podrás hacer una vez completado -

-
- {EXPECTED_BENEFITS.map((benefit, index) => ( -
-
- -
-

- {benefit.title} -

-

- {benefit.description} -

-
- ))} -
-
- )} - - {/* Timeout Warning Modal */} - {showTimeoutWarning && ( -
-
-
- -

- Entrenamiento tomando más tiempo -

-

- Puedes explorar el dashboard mientras terminamos el entrenamiento. - Te notificaremos cuando esté listo. -

-
- - -
-
-
-
- )} -
- ); -} \ No newline at end of file diff --git a/fdev-ffrontend/src/components/ErrorBoundary.tsx b/fdev-ffrontend/src/components/ErrorBoundary.tsx deleted file mode 100644 index af21fc2b..00000000 --- a/fdev-ffrontend/src/components/ErrorBoundary.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// src/components/ErrorBoundary.tsx -import React, { Component, ErrorInfo, ReactNode } from 'react'; -import { AlertTriangle } from 'lucide-react'; - -interface Props { - children: ReactNode; -} - -interface State { - hasError: boolean; - error?: Error; -} - -class ErrorBoundary extends Component { - public state: State = { - hasError: false - }; - - public static getDerivedStateFromError(error: Error): State { - return { hasError: true, error }; - } - - public componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error('ErrorBoundary caught an error:', error, errorInfo); - } - - public render() { - if (this.state.hasError) { - return ( -
-
-
-
- -
-

- ¡Oops! Algo salió mal -

-

- Ha ocurrido un error inesperado. Por favor, recarga la página. -

- - {process.env.NODE_ENV === 'development' && ( -
- - Detalles del error - -
-                    {this.state.error?.stack}
-                  
-
- )} -
-
-
- ); - } - - return this.props.children; - } -} - -export default ErrorBoundary; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/SimplifiedTrainingProgress.tsx b/fdev-ffrontend/src/components/SimplifiedTrainingProgress.tsx deleted file mode 100644 index e912218f..00000000 --- a/fdev-ffrontend/src/components/SimplifiedTrainingProgress.tsx +++ /dev/null @@ -1,415 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { - Sparkles, CheckCircle, Clock, ArrowRight, Coffee, - TrendingUp, Target, Loader, AlertTriangle, Mail, - ChevronDown, ChevronUp, HelpCircle, ExternalLink -} from 'lucide-react'; - -interface SimplifiedTrainingProgressProps { - progress: { - progress: number; - status: string; - currentStep: string; - productsCompleted: number; - productsTotal: number; - estimatedTimeRemaining: number; - error?: string; - }; - onTimeout?: () => void; - onBackgroundMode?: () => void; - onEmailNotification?: (email: string) => void; - // Optional WebSocket debugging info - websocketStatus?: string; - connectionError?: string; - isConnected?: boolean; - onRetryConnection?: () => void; -} - -// Proceso simplificado de entrenamiento en 3 etapas -const TRAINING_STAGES = [ - { - id: 'preparing', - title: 'Preparando Tus Datos', - description: 'Estamos organizando tu historial de ventas para encontrar patrones', - userFriendly: 'Piensa en esto como ordenar tus recibos para entender tus días de mayor venta', - progressRange: [0, 30], - icon: Coffee, - color: 'blue', - celebration: '📊 ¡Los datos están listos!' - }, - { - id: 'learning', - title: 'Construyendo Tu Modelo', - description: 'Tu IA está aprendiendo de tus patrones de ventas', - userFriendly: 'Como enseñar a un asistente inteligente a reconocer cuándo vendes más pan', - progressRange: [30, 80], - icon: Sparkles, - color: 'purple', - celebration: '🧠 ¡Tu IA se está volviendo inteligente!' - }, - { - id: 'finalizing', - title: 'Casi Listo', - description: 'Ajustando las predicciones para tu panadería', - userFriendly: 'Nos aseguramos de que las predicciones funcionen perfectamente para tu negocio específico', - progressRange: [80, 100], - icon: Target, - color: 'green', - celebration: '🎉 ¡Tus predicciones están listas!' - } -]; - -const BENEFITS_PREVIEW = [ - { - icon: TrendingUp, - title: 'Pronósticos Inteligentes', - description: 'Saber exactamente cuánto hornear cada día', - example: 'Nunca te quedes sin croissants los domingos ocupados' - }, - { - icon: Target, - title: 'Reducir Desperdicios', - description: 'Deja de hacer demasiado de productos que se venden poco', - example: 'Ahorra dinero horneando las cantidades correctas' - }, - { - icon: Sparkles, - title: 'Detectar Tendencias', - description: 'Ve qué productos se están volviendo populares', - example: 'Nota cuando los clientes empiezan a amar tu nueva receta' - } -]; - -export default function SimplifiedTrainingProgress({ - progress, - onTimeout, - onBackgroundMode, - onEmailNotification, - websocketStatus, - connectionError, - isConnected, - onRetryConnection -}: SimplifiedTrainingProgressProps) { - const [showDetails, setShowDetails] = useState(false); - const [showTimeoutOptions, setShowTimeoutOptions] = useState(false); - const [emailForNotification, setEmailForNotification] = useState(''); - const [celebratingStage, setCelebratingStage] = useState(null); - const [startTime] = useState(Date.now()); - const celebratedStagesRef = useRef>(new Set()); - - // Show timeout options after 7 minutes for better UX - useEffect(() => { - const timer = setTimeout(() => { - if (progress.status === 'running' && progress.progress < 90) { - setShowTimeoutOptions(true); - } - }, 420000); // 7 minutes - - return () => clearTimeout(timer); - }, [progress.status, progress.progress]); - - // Celebrate stage completions - fixed to prevent infinite re-renders - useEffect(() => { - TRAINING_STAGES.forEach(stage => { - if (progress.progress >= stage.progressRange[1] && - !celebratedStagesRef.current.has(stage.id) && - progress.progress > 0) { - setCelebratingStage(stage.id); - celebratedStagesRef.current.add(stage.id); - setTimeout(() => setCelebratingStage(null), 3000); - } - }); - }, [progress.progress]); - - const getCurrentStage = () => { - return TRAINING_STAGES.find(stage => - progress.progress >= stage.progressRange[0] && - progress.progress < stage.progressRange[1] - ) || TRAINING_STAGES[TRAINING_STAGES.length - 1]; - }; - - const formatTimeRemaining = (timeValue: number): string => { - if (!timeValue || timeValue <= 0) return 'Casi terminado'; - - // Manejar tanto segundos como minutos del backend - // Si el valor es muy grande, probablemente sean segundos; si es pequeño, probablemente sean minutos - const minutes = timeValue > 120 ? Math.floor(timeValue / 60) : Math.floor(timeValue); - - if (minutes <= 1) return 'Menos de un minuto'; - if (minutes < 60) return `Aproximadamente ${minutes} minutos restantes`; - - const hours = Math.floor(minutes / 60); - const remainingMinutes = minutes % 60; - return hours === 1 - ? `Aproximadamente 1 hora ${remainingMinutes > 0 ? `${remainingMinutes} minutos` : ''} restantes`.trim() - : `Aproximadamente ${hours} horas ${remainingMinutes > 0 ? `${remainingMinutes} minutos` : ''} restantes`.trim(); - }; - - const handleEmailNotification = () => { - if (emailForNotification && onEmailNotification) { - onEmailNotification(emailForNotification); - setShowTimeoutOptions(false); - } - }; - - const currentStage = getCurrentStage(); - - // Error State - if (progress.status === 'failed' || progress.error) { - return ( -
-
-
- -
-

- Algo salió mal -

-

- No te preocupes - esto pasa a veces. Nuestro equipo ha sido notificado y lo arreglará rápidamente. -

- -
-

- Puedes intentar iniciar el entrenamiento de nuevo, o contactar a nuestro equipo de soporte si esto sigue pasando. -

-
- - -
-
- ); - } - - return ( -
- {/* Main Progress Card */} -
- {/* Header */} -
-
- -
-

- {currentStage.title} -

-

- {currentStage.description} -

- - {/* Progress Bar */} -
-
- Progreso - {Math.round(progress.progress)}% -
-
-
-
-
- - {formatTimeRemaining(progress.estimatedTimeRemaining)} -
-
-
- - {/* Stage Progress */} -
-
- {TRAINING_STAGES.map((stage, index) => { - const isCompleted = progress.progress >= stage.progressRange[1]; - const isCurrent = stage.id === currentStage.id; - const isCelebrating = celebratingStage === stage.id; - - return ( -
- {isCelebrating && ( -
- {stage.celebration} -
- )} -
- {isCompleted ? ( - - ) : ( - - )} -
- - {stage.title.split(' ')[0]} - - {index < TRAINING_STAGES.length - 1 && ( - - )} -
- ); - })} -
- - {/* Current Stage Explanation */} -
-

- {currentStage.userFriendly} -

-
- - {/* Connection Status Debug Info */} - {(websocketStatus || connectionError) && ( -
-
-
- Estado de conexión: - {connectionError - ? ` Error - ${connectionError}` - : isConnected - ? ' ✅ Conectado a tiempo real' - : ' ⏳ Conectando...'} -
- {connectionError && onRetryConnection && ( - - )} -
-
- )} - - {/* Optional Details */} - - - {showDetails && ( -
-

- Detalles técnicos: Estamos usando aprendizaje automático para analizar tus patrones de ventas, - tendencias estacionales y relaciones entre productos para crear predicciones precisas de demanda. -

- - Aprende más sobre nuestra tecnología de IA - - -
- )} -
-
- - - {/* Benefits Preview */} -
-

- Lo que obtendrás cuando esto termine -

-
- {BENEFITS_PREVIEW.map((benefit, index) => ( -
-
- -
-

{benefit.title}

-

{benefit.description}

-

ej., {benefit.example}

-
- ))} -
-
- - {/* Timeout Options */} - {showTimeoutOptions && ( -
-
- -

¿Tomando más tiempo del esperado?

-

- ¡No te preocupes! Tienes algunas opciones mientras terminamos. -

-
- -
- {/* Continue in Background */} -
-

Continuar en segundo plano

-

- Explora una vista previa de tu panel de control mientras continúa el entrenamiento. -

- -
- - {/* Email Notification */} -
-

Recibir notificación

-

- Te enviaremos un correo cuando tu modelo esté listo. -

-
- setEmailForNotification(e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500" - /> - -
-
-
- -
- -
-
- )} - -
- ); -} \ No newline at end of file diff --git a/fdev-ffrontend/src/components/adaptive/AdaptiveInventoryWidget.tsx b/fdev-ffrontend/src/components/adaptive/AdaptiveInventoryWidget.tsx deleted file mode 100644 index b50ae799..00000000 --- a/fdev-ffrontend/src/components/adaptive/AdaptiveInventoryWidget.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import React from 'react'; -import { Package, AlertTriangle, TrendingDown, Clock, MapPin } from 'lucide-react'; -import { useBakeryType } from '../../hooks/useBakeryType'; - -interface InventoryItem { - id: string; - name: string; - currentStock: number; - minStock: number; - unit: string; - expiryDate?: string; - location?: string; - supplier?: string; - category: 'ingredient' | 'product' | 'packaging'; -} - -interface AdaptiveInventoryWidgetProps { - items: InventoryItem[]; - title?: string; - showAlerts?: boolean; -} - -export const AdaptiveInventoryWidget: React.FC = ({ - items, - title, - showAlerts = true -}) => { - const { isIndividual, isCentral, getInventoryLabel } = useBakeryType(); - - const getStockStatus = (item: InventoryItem) => { - const ratio = item.currentStock / item.minStock; - if (ratio <= 0.2) return 'critical'; - if (ratio <= 0.5) return 'low'; - return 'normal'; - }; - - const getStatusColor = (status: string) => { - switch (status) { - case 'critical': - return 'text-red-600 bg-red-100'; - case 'low': - return 'text-yellow-600 bg-yellow-100'; - default: - return 'text-green-600 bg-green-100'; - } - }; - - const getExpiryWarning = (expiryDate?: string) => { - if (!expiryDate) return null; - - const today = new Date(); - const expiry = new Date(expiryDate); - const daysUntilExpiry = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 3600 * 24)); - - if (daysUntilExpiry <= 1) return 'expires-today'; - if (daysUntilExpiry <= 3) return 'expires-soon'; - return null; - }; - - const getItemIcon = (category: string) => { - if (isIndividual) { - switch (category) { - case 'ingredient': - return '🌾'; - case 'packaging': - return '📦'; - default: - return '🥖'; - } - } else { - switch (category) { - case 'product': - return '🥖'; - case 'packaging': - return '📦'; - default: - return '📋'; - } - } - }; - - const filteredItems = items.filter(item => { - if (isIndividual) { - return item.category === 'ingredient' || item.category === 'packaging'; - } else { - return item.category === 'product' || item.category === 'packaging'; - } - }); - - const lowStockItems = filteredItems.filter(item => getStockStatus(item) !== 'normal'); - - return ( -
- {/* Header */} -
-
- -

- {title || getInventoryLabel()} -

-
- - {showAlerts && lowStockItems.length > 0 && ( -
- - {lowStockItems.length} alertas -
- )} -
- - {/* Items List */} -
- {filteredItems.slice(0, 6).map((item) => { - const stockStatus = getStockStatus(item); - const expiryWarning = getExpiryWarning(item.expiryDate); - - return ( -
-
- {getItemIcon(item.category)} - -
-
-

- {item.name} -

- - {stockStatus !== 'normal' && ( - - {stockStatus === 'critical' ? 'Crítico' : 'Bajo'} - - )} -
- -
- - Stock: {item.currentStock} {item.unit} - - - {item.location && isCentral && ( -
- - {item.location} -
- )} - - {expiryWarning && ( -
- - {expiryWarning === 'expires-today' ? 'Caduca hoy' : 'Caduca pronto'} -
- )} -
- - {item.supplier && isIndividual && ( -
- Proveedor: {item.supplier} -
- )} -
-
- -
-
-
-
-
-
- ); - })} -
- - {/* Quick Stats */} -
-
-
-
{filteredItems.length}
-
- {isIndividual ? 'Ingredientes' : 'Productos'} -
-
-
-
{lowStockItems.length}
-
Stock bajo
-
-
-
- {filteredItems.filter(item => getExpiryWarning(item.expiryDate)).length} -
-
- {isIndividual ? 'Próximos a caducar' : 'Próximos a vencer'} -
-
-
-
- - {/* Action Button */} -
- -
-
- ); -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/adaptive/AdaptiveProductionCard.tsx b/fdev-ffrontend/src/components/adaptive/AdaptiveProductionCard.tsx deleted file mode 100644 index 20cf6599..00000000 --- a/fdev-ffrontend/src/components/adaptive/AdaptiveProductionCard.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React from 'react'; -import { ChefHat, Truck, Clock, Users, Package, MapPin } from 'lucide-react'; -import { useBakeryType } from '../../hooks/useBakeryType'; - -interface ProductionItem { - id: string; - name: string; - quantity: number; - status: 'pending' | 'in_progress' | 'completed'; - scheduledTime?: string; - location?: string; - assignedTo?: string; -} - -interface AdaptiveProductionCardProps { - item: ProductionItem; - onStatusChange?: (id: string, status: string) => void; - onQuantityChange?: (id: string, quantity: number) => void; -} - -export const AdaptiveProductionCard: React.FC = ({ - item, - onStatusChange, - onQuantityChange -}) => { - const { isIndividual, isCentral, getProductionLabel } = useBakeryType(); - - const getStatusColor = (status: string) => { - switch (status) { - case 'pending': - return 'bg-yellow-100 text-yellow-800'; - case 'in_progress': - return 'bg-blue-100 text-blue-800'; - case 'completed': - return 'bg-green-100 text-green-800'; - default: - return 'bg-gray-100 text-gray-800'; - } - }; - - const getIcon = () => { - return isIndividual ? : ; - }; - - const getStatusLabels = () => { - if (isIndividual) { - return { - pending: 'Pendiente', - in_progress: 'Horneando', - completed: 'Terminado' - }; - } else { - return { - pending: 'Pendiente', - in_progress: 'Distribuyendo', - completed: 'Entregado' - }; - } - }; - - const statusLabels = getStatusLabels(); - - return ( -
- {/* Header */} -
-
-
- {getIcon()} -
-
-

{item.name}

-

- {isIndividual ? 'Lote de producción' : 'Envío a puntos de venta'} -

-
-
- - - {statusLabels[item.status as keyof typeof statusLabels]} - -
- - {/* Quantity */} -
-
- - Cantidad: -
-
- {onQuantityChange ? ( - onQuantityChange(item.id, parseInt(e.target.value))} - className="w-20 px-2 py-1 text-sm border border-gray-300 rounded text-right" - min="0" - /> - ) : ( - {item.quantity} - )} - - {isIndividual ? 'unidades' : 'cajas'} - -
-
- - {/* Additional Info for Bakery Type */} - {item.scheduledTime && ( -
- - - {isIndividual ? 'Hora de horneado:' : 'Hora de entrega:'} {item.scheduledTime} - -
- )} - - {item.location && isCentral && ( -
- - Destino: {item.location} -
- )} - - {item.assignedTo && ( -
- - - {isIndividual ? 'Panadero:' : 'Conductor:'} {item.assignedTo} - -
- )} - - {/* Actions */} - {onStatusChange && item.status !== 'completed' && ( -
- {item.status === 'pending' && ( - - )} - - {item.status === 'in_progress' && ( - - )} -
- )} -
- ); -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/alerts/AlertCard.tsx b/fdev-ffrontend/src/components/alerts/AlertCard.tsx deleted file mode 100644 index 77ecab5e..00000000 --- a/fdev-ffrontend/src/components/alerts/AlertCard.tsx +++ /dev/null @@ -1,304 +0,0 @@ -// frontend/src/components/alerts/AlertCard.tsx -/** - * Individual alert/recommendation card component - * Displays alert details with appropriate styling and actions - */ - -import React, { useState } from 'react'; -import { AlertItem, ItemSeverity, ItemType } from '../../types/alerts'; -import { formatDistanceToNow } from 'date-fns'; -import { es } from 'date-fns/locale'; - -interface AlertCardProps { - item: AlertItem; - onAcknowledge: (itemId: string) => void; - onResolve: (itemId: string) => void; - compact?: boolean; - showActions?: boolean; -} - -const getSeverityConfig = (severity: ItemSeverity, itemType: ItemType) => { - if (itemType === 'recommendation') { - switch (severity) { - case 'high': - return { - color: 'bg-blue-50 border-blue-200 text-blue-900', - icon: '💡', - badge: 'bg-blue-100 text-blue-800' - }; - case 'medium': - return { - color: 'bg-blue-50 border-blue-100 text-blue-800', - icon: '💡', - badge: 'bg-blue-50 text-blue-600' - }; - case 'low': - return { - color: 'bg-gray-50 border-gray-200 text-gray-700', - icon: '💡', - badge: 'bg-gray-100 text-gray-600' - }; - default: - return { - color: 'bg-blue-50 border-blue-200 text-blue-900', - icon: '💡', - badge: 'bg-blue-100 text-blue-800' - }; - } - } else { - switch (severity) { - case 'urgent': - return { - color: 'bg-red-50 border-red-300 text-red-900', - icon: '🚨', - badge: 'bg-red-100 text-red-800', - pulse: true - }; - case 'high': - return { - color: 'bg-orange-50 border-orange-200 text-orange-900', - icon: '⚠️', - badge: 'bg-orange-100 text-orange-800' - }; - case 'medium': - return { - color: 'bg-yellow-50 border-yellow-200 text-yellow-900', - icon: '🔔', - badge: 'bg-yellow-100 text-yellow-800' - }; - case 'low': - return { - color: 'bg-green-50 border-green-200 text-green-900', - icon: 'ℹ️', - badge: 'bg-green-100 text-green-800' - }; - default: - return { - color: 'bg-gray-50 border-gray-200 text-gray-700', - icon: '📋', - badge: 'bg-gray-100 text-gray-600' - }; - } - } -}; - -const getStatusConfig = (status: string) => { - switch (status) { - case 'acknowledged': - return { - color: 'bg-blue-100 text-blue-800', - label: 'Reconocido' - }; - case 'resolved': - return { - color: 'bg-green-100 text-green-800', - label: 'Resuelto' - }; - default: - return { - color: 'bg-gray-100 text-gray-800', - label: 'Activo' - }; - } -}; - -export const AlertCard: React.FC = ({ - item, - onAcknowledge, - onResolve, - compact = false, - showActions = true -}) => { - const [isExpanded, setIsExpanded] = useState(false); - const [actionLoading, setActionLoading] = useState(null); - - const severityConfig = getSeverityConfig(item.severity, item.item_type); - const statusConfig = getStatusConfig(item.status); - - const handleAction = async (action: () => void, actionType: string) => { - setActionLoading(actionType); - try { - await action(); - } finally { - setActionLoading(null); - } - }; - - const timeAgo = formatDistanceToNow(new Date(item.timestamp), { - addSuffix: true, - locale: es - }); - - return ( -
- {/* Header */} -
-
-
- {/* Icon and Type Badge */} -
- {severityConfig.icon} -
- -
- {/* Title and Badges */} -
-
-

- {item.title} -

-
- - {item.item_type === 'alert' ? 'Alerta' : 'Recomendación'} - {item.severity} - - - {statusConfig.label} - - - {item.service} - -
-
- - {/* Expand Button */} - {!compact && ( - - )} -
- - {/* Message */} -

- {item.message} -

- - {/* Timestamp */} -

- {timeAgo} • {new Date(item.timestamp).toLocaleString('es-ES')} -

-
-
-
- - {/* Quick Actions */} - {showActions && item.status === 'active' && ( -
- - - -
- )} -
- - {/* Expanded Details */} - {isExpanded && ( -
- {/* Actions */} - {item.actions.length > 0 && ( -
-

Acciones sugeridas:

-
    - {item.actions.map((action, index) => ( -
  • - {action} -
  • - ))} -
-
- )} - - {/* Metadata */} - {Object.keys(item.metadata).length > 0 && ( -
-

Detalles técnicos:

-
- {Object.entries(item.metadata).map(([key, value]) => ( -
- {key}:{' '} - - {typeof value === 'object' ? JSON.stringify(value) : String(value)} - -
- ))} -
-
- )} - - {/* Acknowledgment/Resolution Info */} - {(item.acknowledged_at || item.resolved_at) && ( -
- {item.acknowledged_at && ( -

- Reconocido: {new Date(item.acknowledged_at).toLocaleString('es-ES')} - {item.acknowledged_by && ` por ${item.acknowledged_by}`} -

- )} - {item.resolved_at && ( -

- Resuelto: {new Date(item.resolved_at).toLocaleString('es-ES')} - {item.resolved_by && ` por ${item.resolved_by}`} -

- )} -
- )} -
- )} -
- ); -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/alerts/AlertDashboard.tsx b/fdev-ffrontend/src/components/alerts/AlertDashboard.tsx deleted file mode 100644 index 32df033c..00000000 --- a/fdev-ffrontend/src/components/alerts/AlertDashboard.tsx +++ /dev/null @@ -1,347 +0,0 @@ -// frontend/src/components/alerts/AlertDashboard.tsx -/** - * Main dashboard component for alerts and recommendations - * Provides filtering, bulk actions, and real-time updates - */ - -import React, { useState, useEffect, useMemo } from 'react'; -import { AlertItem, ItemFilters, ItemType, ItemSeverity, ItemStatus } from '../../types/alerts'; -import { useAlertStream } from '../../hooks/useAlertStream'; -import { AlertCard } from './AlertCard'; -import { AlertFilters } from './AlertFilters'; -import { AlertStats } from './AlertStats'; -import { ConnectionStatus } from './ConnectionStatus'; -import { useTenantId } from '../../hooks/useTenantId'; - -interface AlertDashboardProps { - className?: string; - maxItems?: number; - autoRequestNotifications?: boolean; -} - -export const AlertDashboard: React.FC = ({ - className = '', - maxItems = 50, - autoRequestNotifications = true -}) => { - const tenantId = useTenantId(); - const { - items, - connectionState, - urgentCount, - highCount, - recCount, - acknowledgeItem, - resolveItem, - notificationPermission, - requestNotificationPermission - } = useAlertStream({ tenantId }); - - const [filters, setFilters] = useState({ - item_type: 'all', - severity: 'all', - status: 'all', - service: 'all', - search: '' - }); - - const [selectedItems, setSelectedItems] = useState([]); - const [bulkActionsOpen, setBulkActionsOpen] = useState(false); - const [viewMode, setViewMode] = useState<'list' | 'compact'>('list'); - - // Request notification permission on mount if needed - useEffect(() => { - if (autoRequestNotifications && notificationPermission === 'default') { - // Delay request to avoid immediate popup - const timer = setTimeout(() => { - requestNotificationPermission(); - }, 2000); - return () => clearTimeout(timer); - } - }, [autoRequestNotifications, notificationPermission, requestNotificationPermission]); - - // Filter items based on current filters - const filteredItems = useMemo(() => { - let filtered = items; - - // Filter by type - if (filters.item_type !== 'all') { - filtered = filtered.filter(item => item.item_type === filters.item_type); - } - - // Filter by severity - if (filters.severity !== 'all') { - filtered = filtered.filter(item => item.severity === filters.severity); - } - - // Filter by status - if (filters.status !== 'all') { - filtered = filtered.filter(item => item.status === filters.status); - } - - // Filter by service - if (filters.service !== 'all') { - filtered = filtered.filter(item => item.service === filters.service); - } - - // Filter by search text - if (filters.search.trim()) { - const searchLower = filters.search.toLowerCase(); - filtered = filtered.filter(item => - item.title.toLowerCase().includes(searchLower) || - item.message.toLowerCase().includes(searchLower) || - item.type.toLowerCase().includes(searchLower) - ); - } - - return filtered.slice(0, maxItems); - }, [items, filters, maxItems]); - - // Get unique services for filter dropdown - const availableServices = useMemo(() => { - const services = [...new Set(items.map(item => item.service))].sort(); - return services; - }, [items]); - - // Handle bulk actions - const handleBulkAcknowledge = async () => { - await Promise.all(selectedItems.map(id => acknowledgeItem(id))); - setSelectedItems([]); - setBulkActionsOpen(false); - }; - - const handleBulkResolve = async () => { - await Promise.all(selectedItems.map(id => resolveItem(id))); - setSelectedItems([]); - setBulkActionsOpen(false); - }; - - const handleSelectAll = () => { - const selectableItems = filteredItems - .filter(item => item.status === 'active') - .map(item => item.id); - setSelectedItems(selectableItems); - }; - - const handleClearSelection = () => { - setSelectedItems([]); - setBulkActionsOpen(false); - }; - - const toggleItemSelection = (itemId: string) => { - setSelectedItems(prev => - prev.includes(itemId) - ? prev.filter(id => id !== itemId) - : [...prev, itemId] - ); - }; - - const activeItems = filteredItems.filter(item => item.status === 'active'); - const hasSelection = selectedItems.length > 0; - - return ( -
- {/* Header */} -
-
-
-

- Sistema de Alertas y Recomendaciones -

-

- Monitoreo en tiempo real de operaciones de panadería -

-
- - {/* Connection Status */} - -
-
- - {/* Stats */} - - - {/* Notification Permission Banner */} - {notificationPermission === 'denied' && ( -
-
-
- - - -
-
-

- Notificaciones bloqueadas -

-

- Las notificaciones del navegador están deshabilitadas. No recibirás alertas urgentes en tiempo real. -

-
-
-
- )} - - {/* Filters and View Controls */} -
-
- - -
- {/* View Mode Toggle */} -
- - -
- - {/* Bulk Actions */} - {activeItems.length > 0 && ( -
- -
- )} -
-
- - {/* Bulk Actions Panel */} - {bulkActionsOpen && activeItems.length > 0 && ( -
-
-
- - {selectedItems.length} elementos seleccionados - - - -
- - {hasSelection && ( -
- - -
- )} -
-
- )} -
- - {/* Items List */} -
- {filteredItems.length === 0 ? ( -
- {items.length === 0 ? ( -
- - - -

- Sistema operativo -

-

- No hay alertas activas en este momento. Todas las operaciones funcionan correctamente. -

-
- ) : ( -
- - - -

- No se encontraron elementos -

-

- Intenta ajustar los filtros para ver más elementos. -

-
- )} -
- ) : ( -
- {filteredItems.map((item) => ( -
- {/* Selection Checkbox */} - {bulkActionsOpen && item.status === 'active' && ( -
- toggleItemSelection(item.id)} - className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" - /> -
- )} - -
- -
-
- ))} -
- )} -
-
- ); -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/alerts/AlertFilters.tsx b/fdev-ffrontend/src/components/alerts/AlertFilters.tsx deleted file mode 100644 index 47867d98..00000000 --- a/fdev-ffrontend/src/components/alerts/AlertFilters.tsx +++ /dev/null @@ -1,148 +0,0 @@ -// frontend/src/components/alerts/AlertFilters.tsx -/** - * Filter controls for the alert dashboard - */ - -import React from 'react'; -import { ItemFilters, ItemType, ItemSeverity, ItemStatus } from '../../types/alerts'; - -interface AlertFiltersProps { - filters: ItemFilters; - onFiltersChange: (filters: ItemFilters) => void; - availableServices: string[]; -} - -export const AlertFilters: React.FC = ({ - filters, - onFiltersChange, - availableServices -}) => { - const updateFilter = (key: keyof ItemFilters, value: string) => { - onFiltersChange({ - ...filters, - [key]: value - }); - }; - - return ( -
- {/* Search */} -
- -
-
- - - -
- updateFilter('search', e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm" - /> -
-
- - {/* Type Filter */} -
- - -
- - {/* Severity Filter */} -
- - -
- - {/* Status Filter */} -
- - -
- - {/* Service Filter */} - {availableServices.length > 0 && ( -
- - -
- )} - - {/* Clear Filters */} - {(filters.search || filters.item_type !== 'all' || filters.severity !== 'all' || - filters.status !== 'all' || filters.service !== 'all') && ( - - )} -
- ); -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/alerts/AlertStats.tsx b/fdev-ffrontend/src/components/alerts/AlertStats.tsx deleted file mode 100644 index e76e3afa..00000000 --- a/fdev-ffrontend/src/components/alerts/AlertStats.tsx +++ /dev/null @@ -1,102 +0,0 @@ -// frontend/src/components/alerts/AlertStats.tsx -/** - * Statistics display for alerts and recommendations - */ - -import React from 'react'; - -interface AlertStatsProps { - urgentCount: number; - highCount: number; - recCount: number; - totalItems: number; - activeItems: number; -} - -export const AlertStats: React.FC = ({ - urgentCount, - highCount, - recCount, - totalItems, - activeItems -}) => { - const stats = [ - { - name: 'Alertas Urgentes', - value: urgentCount, - icon: '🚨', - color: urgentCount > 0 ? 'text-red-600' : 'text-gray-600', - bgColor: urgentCount > 0 ? 'bg-red-50' : 'bg-gray-50', - borderColor: urgentCount > 0 ? 'border-red-200' : 'border-gray-200' - }, - { - name: 'Alertas Altas', - value: highCount, - icon: '⚠️', - color: highCount > 0 ? 'text-orange-600' : 'text-gray-600', - bgColor: highCount > 0 ? 'bg-orange-50' : 'bg-gray-50', - borderColor: highCount > 0 ? 'border-orange-200' : 'border-gray-200' - }, - { - name: 'Recomendaciones', - value: recCount, - icon: '💡', - color: recCount > 0 ? 'text-blue-600' : 'text-gray-600', - bgColor: recCount > 0 ? 'bg-blue-50' : 'bg-gray-50', - borderColor: recCount > 0 ? 'border-blue-200' : 'border-gray-200' - }, - { - name: 'Total Activos', - value: activeItems, - icon: '📊', - color: 'text-gray-600', - bgColor: 'bg-gray-50', - borderColor: 'border-gray-200' - } - ]; - - return ( -
-
-
- {stats.map((stat) => ( -
-
- {stat.icon} - {stat.name} -
-
- {stat.value} -
- - {/* Pulse animation for urgent alerts */} - {stat.name === 'Alertas Urgentes' && urgentCount > 0 && ( -
- )} -
- ))} -
- - {/* Summary text */} -
- {totalItems === 0 ? ( -

- - Todos los sistemas funcionan correctamente -

- ) : ( -

- Mostrando {totalItems} elementos total{totalItems !== 1 ? 'es' : ''} - {activeItems > 0 && ( - <>, {activeItems} activo{activeItems !== 1 ? 's' : ''} - )} -

- )} -
-
-
- ); -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/alerts/ConnectionStatus.tsx b/fdev-ffrontend/src/components/alerts/ConnectionStatus.tsx deleted file mode 100644 index 9bd35c58..00000000 --- a/fdev-ffrontend/src/components/alerts/ConnectionStatus.tsx +++ /dev/null @@ -1,70 +0,0 @@ -// frontend/src/components/alerts/ConnectionStatus.tsx -/** - * Displays the current SSE connection status with appropriate styling - */ - -import React from 'react'; -import { SSEConnectionState } from '../../types/alerts'; - -interface ConnectionStatusProps { - connectionState: SSEConnectionState; -} - -export const ConnectionStatus: React.FC = ({ - connectionState -}) => { - const getStatusConfig = (state: SSEConnectionState) => { - switch (state.status) { - case 'connected': - return { - color: 'bg-green-100 text-green-800 border-green-200', - icon: '🟢', - label: 'Conectado', - description: 'Actualizaciones en tiempo real' - }; - case 'connecting': - return { - color: 'bg-yellow-100 text-yellow-800 border-yellow-200', - icon: '🟡', - label: 'Conectando...', - description: 'Estableciendo conexión' - }; - case 'error': - return { - color: 'bg-red-100 text-red-800 border-red-200', - icon: '🔴', - label: 'Error de conexión', - description: state.reconnectAttempts > 0 ? `Reintento ${state.reconnectAttempts}` : 'Fallo en la conexión' - }; - case 'disconnected': - default: - return { - color: 'bg-gray-100 text-gray-800 border-gray-200', - icon: '⚪', - label: 'Desconectado', - description: 'Sin actualizaciones en tiempo real' - }; - } - }; - - const config = getStatusConfig(connectionState); - - return ( -
- {config.icon} -
- {config.label} - {config.description} -
- - {connectionState.status === 'connecting' && ( -
- - - - -
- )} -
- ); -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/auth/ProtectedRoute.tsx b/fdev-ffrontend/src/components/auth/ProtectedRoute.tsx deleted file mode 100644 index 847284e1..00000000 --- a/fdev-ffrontend/src/components/auth/ProtectedRoute.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Navigate, useLocation } from 'react-router-dom'; -import { useSelector, useDispatch } from 'react-redux'; -import { RootState } from '../../store'; -import { completeOnboarding } from '../../store/slices/authSlice'; -import { OnboardingRouter } from '../../utils/onboardingRouter'; - -interface ProtectedRouteProps { - children: React.ReactNode; -} - -const ProtectedRoute: React.FC = ({ children }) => { - const location = useLocation(); - const dispatch = useDispatch(); - const { isAuthenticated, user } = useSelector((state: RootState) => state.auth); - const [onboardingCheck, setOnboardingCheck] = useState<'checking' | 'complete' | 'incomplete'>('checking'); - - // Check if user is authenticated - if (!isAuthenticated || !user) { - // Redirect to login with the attempted location - return ; - } - - // Sync onboarding status with backend on mount and navigation - useEffect(() => { - const checkOnboardingStatus = async () => { - try { - // If user already marked as onboarding complete, skip API check - if (user.isOnboardingComplete) { - setOnboardingCheck('complete'); - return; - } - - // Check actual onboarding progress from backend - const canAccess = await OnboardingRouter.canAccessDashboard(); - - if (canAccess) { - // User has completed onboarding according to backend, update Redux state - dispatch(completeOnboarding()); - setOnboardingCheck('complete'); - } else { - setOnboardingCheck('incomplete'); - } - } catch (error) { - console.error('Error checking onboarding status:', error); - // On error, use current Redux state - setOnboardingCheck(user.isOnboardingComplete ? 'complete' : 'incomplete'); - } - }; - - checkOnboardingStatus(); - }, [user.isOnboardingComplete, dispatch]); - - // Show loading while checking onboarding status - if (onboardingCheck === 'checking') { - return ( -
-
-
- ); - } - - // Route-based logic - const isOnboardingRoute = location.pathname.includes('/onboarding'); - const isSettingsRoute = location.pathname.includes('/settings'); - - // If onboarding not complete and not on onboarding/settings route, redirect to onboarding - if (onboardingCheck === 'incomplete' && !isOnboardingRoute && !isSettingsRoute) { - return ; - } - - // If onboarding complete but on onboarding route, redirect to dashboard - if (onboardingCheck === 'complete' && isOnboardingRoute) { - return ; - } - - return <>{children}; -}; - -export default ProtectedRoute; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/auth/RoleBasedAccess.tsx b/fdev-ffrontend/src/components/auth/RoleBasedAccess.tsx deleted file mode 100644 index 9ecf64ad..00000000 --- a/fdev-ffrontend/src/components/auth/RoleBasedAccess.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import { usePermissions, UserRole } from '../../hooks/usePermissions'; - -interface RoleBasedAccessProps { - children: React.ReactNode; - requiredRoles?: UserRole[]; - requiredPermissions?: string[]; - fallback?: React.ReactNode; - hideIfNoAccess?: boolean; -} - -export const RoleBasedAccess: React.FC = ({ - children, - requiredRoles = [], - requiredPermissions = [], - fallback = null, - hideIfNoAccess = false -}) => { - const { hasRole, hasPermission } = usePermissions(); - - // Check role requirements - const hasRequiredRole = requiredRoles.length === 0 || requiredRoles.some(role => hasRole(role)); - - // Check permission requirements - const hasRequiredPermission = requiredPermissions.length === 0 || requiredPermissions.some(permission => hasPermission(permission)); - - const hasAccess = hasRequiredRole && hasRequiredPermission; - - if (!hasAccess) { - if (hideIfNoAccess) { - return null; - } - return <>{fallback}; - } - - return <>{children}; -}; - -// Convenience components for common use cases -export const AdminOnly: React.FC<{ children: React.ReactNode; fallback?: React.ReactNode }> = ({ children, fallback }) => ( - - {children} - -); - -export const ManagerAndUp: React.FC<{ children: React.ReactNode; fallback?: React.ReactNode }> = ({ children, fallback }) => ( - - {children} - -); - -export const OwnerOnly: React.FC<{ children: React.ReactNode; fallback?: React.ReactNode }> = ({ children, fallback }) => ( - - {children} - -); - -export default RoleBasedAccess; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/auth/RoleBasedRoute.tsx b/fdev-ffrontend/src/components/auth/RoleBasedRoute.tsx deleted file mode 100644 index ffc6a12d..00000000 --- a/fdev-ffrontend/src/components/auth/RoleBasedRoute.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { Navigate } from 'react-router-dom'; -import { useSelector } from 'react-redux'; -import { RootState } from '../../store'; -import { usePermissions } from '../../hooks/usePermissions'; - -interface RoleBasedRouteProps { - children: React.ReactNode; - requiredRoles?: string[]; - requiredPermissions?: string[]; - fallbackPath?: string; -} - -const RoleBasedRoute: React.FC = ({ - children, - requiredRoles = [], - requiredPermissions = [], - fallbackPath = '/app/dashboard' -}) => { - const { user } = useSelector((state: RootState) => state.auth); - const { hasRole, hasPermission } = usePermissions(); - - // Check role requirements - if (requiredRoles.length > 0) { - const hasRequiredRole = requiredRoles.some(role => hasRole(role)); - if (!hasRequiredRole) { - return ; - } - } - - // Check permission requirements - if (requiredPermissions.length > 0) { - const hasRequiredPermission = requiredPermissions.some(permission => hasPermission(permission)); - if (!hasRequiredPermission) { - return ; - } - } - - return <>{children}; -}; - -export default RoleBasedRoute; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/inventory/InventoryDashboardWidget.tsx b/fdev-ffrontend/src/components/inventory/InventoryDashboardWidget.tsx deleted file mode 100644 index 57814d43..00000000 --- a/fdev-ffrontend/src/components/inventory/InventoryDashboardWidget.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import React from 'react'; -import { - Package, - TrendingDown, - AlertTriangle, - Calendar, - BarChart3, - ArrowRight, - Loader, - RefreshCw -} from 'lucide-react'; - -import { useInventoryDashboard } from '../../api/hooks/useInventory'; - -interface InventoryDashboardWidgetProps { - onViewInventory?: () => void; - className?: string; -} - -const InventoryDashboardWidget: React.FC = ({ - onViewInventory, - className = '' -}) => { - const { dashboardData, isLoading, error, refresh } = useInventoryDashboard(); - - // Get alert counts - const criticalAlerts = 0; - const lowStockAlerts = 0; - const expiringAlerts = 0; - - if (isLoading) { - return ( -
-
- -

Inventario

-
- -
- - Cargando datos... -
-
- ); - } - - if (error) { - return ( -
-
-
- -

Inventario

-
- -
- -
- -

Error al cargar datos de inventario

- -
-
- ); - } - - return ( -
- {/* Header */} -
-
-
- -

Inventario

-
- -
- - - {onViewInventory && ( - - )} -
-
- - {/* Quick Stats */} -
-
-
- {dashboardData?.total_items || 0} -
-
Total Productos
-
- -
-
- €{(dashboardData?.total_value || 0).toLocaleString()} -
-
Valor Total
-
-
- - {/* Alerts Summary */} - {criticalAlerts > 0 || lowStockAlerts > 0 || expiringAlerts > 0 ? ( -
-

- - Alertas Activas -

- -
- {criticalAlerts > 0 && ( -
-
-
- Críticas -
- {criticalAlerts} -
- )} - - {lowStockAlerts > 0 && ( -
-
- - Stock Bajo -
- {lowStockAlerts} -
- )} - - {expiringAlerts > 0 && ( -
-
- - Por Vencer -
- {expiringAlerts} -
- )} -
-
- ) : ( -
-
- -
-

Todo en orden

-

No hay alertas activas en tu inventario

-
- )} - - {/* Top Categories */} - {dashboardData?.category_breakdown && dashboardData.category_breakdown.length > 0 && ( -
-

- - Top Categorías por Valor -

- -
- {dashboardData.category_breakdown.slice(0, 3).map((category, index) => ( -
-
-
- - {category.category} - -
-
-
- €{category.value.toLocaleString()} -
-
- {category.count} productos -
-
-
- ))} -
-
- )} - - {/* Recent Activity */} - {dashboardData?.recent_movements && dashboardData.recent_movements.length > 0 && ( -
-

Actividad Reciente

- -
- {dashboardData.recent_movements.slice(0, 3).map((movement) => ( -
-
- {movement.movement_type === 'purchase' ? '+' : - movement.movement_type === 'consumption' ? '-' : - movement.movement_type === 'waste' ? '×' : - '~'} -
- -
-
- {movement.item_name || 'Producto'} -
-
- {movement.quantity} • {new Date(movement.movement_date).toLocaleDateString()} -
-
-
- ))} -
-
- )} -
-
- ); -}; - -export default InventoryDashboardWidget; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/inventory/InventoryItemCard.tsx b/fdev-ffrontend/src/components/inventory/InventoryItemCard.tsx deleted file mode 100644 index b3638208..00000000 --- a/fdev-ffrontend/src/components/inventory/InventoryItemCard.tsx +++ /dev/null @@ -1,424 +0,0 @@ -import React, { useState } from 'react'; -import { - Package, - AlertTriangle, - Clock, - Thermometer, - Snowflake, - Calendar, - TrendingDown, - TrendingUp, - Edit3, - Trash2, - Plus, - Minus, - Eye, - MoreVertical -} from 'lucide-react'; - -import { - InventoryItem, - StockLevel, - ProductType, - StockAdjustmentRequest -} from '../../api/services/inventory.service'; - -interface InventoryItemCardProps { - item: InventoryItem; - stockLevel?: StockLevel; - compact?: boolean; - showActions?: boolean; - onEdit?: (item: InventoryItem) => void; - onDelete?: (item: InventoryItem) => void; - onViewDetails?: (item: InventoryItem) => void; - onStockAdjust?: (item: InventoryItem, adjustment: StockAdjustmentRequest) => void; - className?: string; -} - -const InventoryItemCard: React.FC = ({ - item, - stockLevel, - compact = false, - showActions = true, - onEdit, - onDelete, - onViewDetails, - onStockAdjust, - className = '' -}) => { - const [showQuickAdjust, setShowQuickAdjust] = useState(false); - const [adjustmentQuantity, setAdjustmentQuantity] = useState(''); - - // Get stock status - const getStockStatus = () => { - if (!stockLevel) return null; - - const { current_quantity, available_quantity } = stockLevel; - const { minimum_stock_level, reorder_point } = item; - - if (current_quantity <= 0) { - return { status: 'out_of_stock', label: 'Sin stock', color: 'red' }; - } - - if (minimum_stock_level && current_quantity <= minimum_stock_level) { - return { status: 'low_stock', label: 'Stock bajo', color: 'yellow' }; - } - - if (reorder_point && current_quantity <= reorder_point) { - return { status: 'reorder', label: 'Reordenar', color: 'orange' }; - } - - return { status: 'good', label: 'Stock OK', color: 'green' }; - }; - - const stockStatus = getStockStatus(); - - // Get expiration status - const getExpirationStatus = () => { - if (!stockLevel?.batches || stockLevel.batches.length === 0) return null; - - const expiredBatches = stockLevel.batches.filter(b => b.is_expired); - const expiringSoon = stockLevel.batches.filter(b => - !b.is_expired && b.days_until_expiration !== undefined && b.days_until_expiration <= 3 - ); - - if (expiredBatches.length > 0) { - return { status: 'expired', label: 'Vencido', color: 'red' }; - } - - if (expiringSoon.length > 0) { - return { status: 'expiring', label: 'Por vencer', color: 'yellow' }; - } - - return null; - }; - - const expirationStatus = getExpirationStatus(); - - // Get category display info - const getCategoryInfo = () => { - const categoryLabels: Record = { - // Ingredients - flour: 'Harina', - yeast: 'Levadura', - dairy: 'Lácteos', - eggs: 'Huevos', - sugar: 'Azúcar', - fats: 'Grasas', - salt: 'Sal', - spices: 'Especias', - additives: 'Aditivos', - packaging: 'Embalaje', - - // Finished Products - bread: 'Pan', - croissants: 'Croissants', - pastries: 'Repostería', - cakes: 'Tartas', - cookies: 'Galletas', - muffins: 'Magdalenas', - sandwiches: 'Sandwiches', - beverages: 'Bebidas', - other_products: 'Otros' - }; - - return categoryLabels[item.category] || item.category; - }; - - // Handle quick stock adjustment - const handleQuickAdjust = (type: 'add' | 'remove') => { - if (!adjustmentQuantity || !onStockAdjust) return; - - const quantity = parseFloat(adjustmentQuantity); - if (isNaN(quantity) || quantity <= 0) return; - - const adjustment: StockAdjustmentRequest = { - movement_type: type === 'add' ? 'purchase' : 'consumption', - quantity: type === 'add' ? quantity : -quantity, - notes: `Quick ${type === 'add' ? 'addition' : 'consumption'} via inventory card` - }; - - onStockAdjust(item, adjustment); - setAdjustmentQuantity(''); - setShowQuickAdjust(false); - }; - - if (compact) { - return ( -
-
-
-
- -
-
-

{item.name}

-

{getCategoryInfo()}

-
-
- -
- {stockLevel && ( -
-
- {stockLevel.current_quantity} {stockLevel.unit_of_measure} -
- {stockStatus && ( -
- {stockStatus.label} -
- )} -
- )} - - {showActions && onViewDetails && ( - - )} -
-
-
- ); - } - - return ( -
- {/* Header */} -
-
-
-
- -
- -
-
-

{item.name}

- {!item.is_active && ( - - Inactivo - - )} -
- -
- - {item.product_type === 'ingredient' ? 'Ingrediente' : 'Producto Final'} - - {getCategoryInfo()} - {item.unit_of_measure} -
- - {/* Special requirements */} - {(item.requires_refrigeration || item.requires_freezing || item.is_seasonal) && ( -
- {item.requires_refrigeration && ( -
- - Refrigeración -
- )} - {item.requires_freezing && ( -
- - Congelación -
- )} - {item.is_seasonal && ( -
- - Estacional -
- )} -
- )} -
-
- - {showActions && ( -
- {onEdit && ( - - )} - - {onViewDetails && ( - - )} - - -
- )} -
-
- - {/* Stock Information */} - {stockLevel && ( -
-
-

Stock Actual

- - {(stockStatus || expirationStatus) && ( -
- {expirationStatus && ( -
- - {expirationStatus.label} -
- )} - - {stockStatus && ( -
- {stockStatus.color === 'red' ? : - stockStatus.color === 'green' ? : - } - {stockStatus.label} -
- )} -
- )} -
- -
-
-
- {stockLevel.current_quantity} -
-
Cantidad Total
-
-
-
- {stockLevel.available_quantity} -
-
Disponible
-
-
-
- {stockLevel.reserved_quantity} -
-
Reservado
-
-
- - {/* Stock Levels */} - {(item.minimum_stock_level || item.reorder_point) && ( -
- {item.minimum_stock_level && ( - Mínimo: {item.minimum_stock_level} - )} - {item.reorder_point && ( - Reorden: {item.reorder_point} - )} -
- )} - - {/* Quick Adjust */} - {showActions && onStockAdjust && ( -
- {!showQuickAdjust ? ( - - ) : ( -
-
- setAdjustmentQuantity(e.target.value)} - placeholder="Cantidad" - className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> - {item.unit_of_measure} -
- -
- - - -
-
- )} -
- )} -
- )} - - {/* No Stock Data */} - {!stockLevel && ( -
-
- -

No hay datos de stock

-
-
- )} -
- ); -}; - -export default InventoryItemCard; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/layout/AnalyticsLayout.tsx b/fdev-ffrontend/src/components/layout/AnalyticsLayout.tsx deleted file mode 100644 index d8797446..00000000 --- a/fdev-ffrontend/src/components/layout/AnalyticsLayout.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { Outlet } from 'react-router-dom'; -import { SecondaryNavigation } from '../navigation/SecondaryNavigation'; -import { Breadcrumbs } from '../navigation/Breadcrumbs'; -import { useBakeryType } from '../../hooks/useBakeryType'; - -const AnalyticsLayout: React.FC = () => { - const { bakeryType } = useBakeryType(); - - const navigationItems = [ - { - id: 'forecasting', - label: 'Predicciones', - href: '/app/analytics/forecasting', - icon: 'TrendingUp' - }, - { - id: 'sales-analytics', - label: 'Análisis Ventas', - href: '/app/analytics/sales-analytics', - icon: 'BarChart3' - }, - { - id: 'production-reports', - label: bakeryType === 'individual' ? 'Reportes Producción' : 'Reportes Distribución', - href: '/app/analytics/production-reports', - icon: 'FileBarChart' - }, - { - id: 'financial-reports', - label: 'Reportes Financieros', - href: '/app/analytics/financial-reports', - icon: 'DollarSign' - }, - { - id: 'performance-kpis', - label: 'KPIs Rendimiento', - href: '/app/analytics/performance-kpis', - icon: 'Target' - }, - { - id: 'ai-insights', - label: 'Insights IA', - href: '/app/analytics/ai-insights', - icon: 'Brain' - } - ]; - - return ( -
-
-
- - -
-
- -
-
- -
-
-
- ); -}; - -export default AnalyticsLayout; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/layout/AuthLayout.tsx b/fdev-ffrontend/src/components/layout/AuthLayout.tsx deleted file mode 100644 index 368b2a09..00000000 --- a/fdev-ffrontend/src/components/layout/AuthLayout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { Outlet } from 'react-router-dom'; - -const AuthLayout: React.FC = () => { - return ( -
- -
- ); -}; - -export default AuthLayout; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/layout/Layout.tsx b/fdev-ffrontend/src/components/layout/Layout.tsx deleted file mode 100644 index 5cbde239..00000000 --- a/fdev-ffrontend/src/components/layout/Layout.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import React, { useState } from 'react'; -import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom'; -import { - Home, - TrendingUp, - Package, - Settings, - Menu, - X, - LogOut, - User, - Bell, - ChevronDown, - BarChart3, - Building -} from 'lucide-react'; -import { useSelector, useDispatch } from 'react-redux'; -import { RootState } from '../../store'; -import { logout } from '../../store/slices/authSlice'; -import { TenantSelector } from '../navigation/TenantSelector'; -import { usePermissions } from '../../hooks/usePermissions'; - -interface LayoutProps { - // No props needed - using React Router -} - -interface NavigationItem { - id: string; - label: string; - icon: React.ComponentType<{ className?: string }>; - href: string; - requiresRole?: string[]; -} - -const Layout: React.FC = () => { - const location = useLocation(); - const navigate = useNavigate(); - const dispatch = useDispatch(); - const { user } = useSelector((state: RootState) => state.auth); - const { hasRole } = usePermissions(); - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); - const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); - - const navigation: NavigationItem[] = [ - { id: 'dashboard', label: 'Dashboard', icon: Home, href: '/app/dashboard' }, - { id: 'operations', label: 'Operaciones', icon: Package, href: '/app/operations' }, - { - id: 'analytics', - label: 'Analytics', - icon: BarChart3, - href: '/app/analytics', - requiresRole: ['admin', 'manager'] - }, - { id: 'settings', label: 'Configuración', icon: Settings, href: '/app/settings' }, - ]; - - // Filter navigation based on user role - const filteredNavigation = navigation.filter(item => { - if (!item.requiresRole) return true; - return item.requiresRole.some(role => hasRole(role)); - }); - - const handleLogout = () => { - if (window.confirm('¿Estás seguro de que quieres cerrar sesión?')) { - dispatch(logout()); - localStorage.removeItem('auth_token'); - localStorage.removeItem('user_data'); - localStorage.removeItem('selectedTenantId'); - navigate('/'); - } - }; - - const isActiveRoute = (href: string): boolean => { - if (href === '/app/dashboard') { - return location.pathname === '/app/dashboard' || location.pathname === '/app'; - } - return location.pathname.startsWith(href); - }; - - return ( -
- {/* Top Navigation Bar */} - - - {/* Main Content */} -
- -
- - {/* Click outside handler for dropdowns */} - {(isUserMenuOpen || isMobileMenuOpen) && ( -
{ - setIsUserMenuOpen(false); - setIsMobileMenuOpen(false); - }} - /> - )} -
- ); -}; - -export default Layout; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/layout/OperationsLayout.tsx b/fdev-ffrontend/src/components/layout/OperationsLayout.tsx deleted file mode 100644 index 432569e2..00000000 --- a/fdev-ffrontend/src/components/layout/OperationsLayout.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react'; -import { Outlet } from 'react-router-dom'; -import { SecondaryNavigation } from '../navigation/SecondaryNavigation'; -import { Breadcrumbs } from '../navigation/Breadcrumbs'; -import { useBakeryType } from '../../hooks/useBakeryType'; - -const OperationsLayout: React.FC = () => { - const { bakeryType } = useBakeryType(); - - // Define navigation items based on bakery type - const getNavigationItems = () => { - const baseItems = [ - { - id: 'production', - label: bakeryType === 'individual' ? 'Producción' : 'Distribución', - href: '/app/operations/production', - icon: 'ChefHat', - children: bakeryType === 'individual' ? [ - { id: 'schedule', label: 'Programación', href: '/app/operations/production/schedule' }, - { id: 'active-batches', label: 'Lotes Activos', href: '/app/operations/production/active-batches' }, - { id: 'equipment', label: 'Equipamiento', href: '/app/operations/production/equipment' } - ] : [ - { id: 'schedule', label: 'Distribución', href: '/app/operations/production/schedule' }, - { id: 'active-batches', label: 'Asignaciones', href: '/app/operations/production/active-batches' }, - { id: 'equipment', label: 'Logística', href: '/app/operations/production/equipment' } - ] - }, - { - id: 'orders', - label: 'Pedidos', - href: '/app/operations/orders', - icon: 'Package', - children: [ - { id: 'incoming', label: bakeryType === 'individual' ? 'Entrantes' : 'Puntos de Venta', href: '/app/operations/orders/incoming' }, - { id: 'in-progress', label: 'En Proceso', href: '/app/operations/orders/in-progress' }, - { id: 'supplier-orders', label: bakeryType === 'individual' ? 'Proveedores' : 'Productos', href: '/app/operations/orders/supplier-orders' } - ] - }, - { - id: 'inventory', - label: 'Inventario', - href: '/app/operations/inventory', - icon: 'Warehouse', - children: [ - { id: 'stock-levels', label: bakeryType === 'individual' ? 'Ingredientes' : 'Productos', href: '/app/operations/inventory/stock-levels' }, - { id: 'movements', label: bakeryType === 'individual' ? 'Uso' : 'Distribución', href: '/app/operations/inventory/movements' }, - { id: 'alerts', label: bakeryType === 'individual' ? 'Caducidad' : 'Retrasos', href: '/app/operations/inventory/alerts' } - ] - }, - { - id: 'sales', - label: 'Ventas', - href: '/app/operations/sales', - icon: 'ShoppingCart', - children: [ - { id: 'daily-sales', label: 'Ventas Diarias', href: '/app/operations/sales/daily-sales' }, - { id: 'customer-orders', label: bakeryType === 'individual' ? 'Pedidos Cliente' : 'Pedidos Punto', href: '/app/operations/sales/customer-orders' } - ] - }, - { - id: 'pos', - label: bakeryType === 'individual' ? 'TPV' : 'Sistema TPV', - href: '/app/operations/pos', - icon: 'CreditCard', - children: [ - { id: 'integrations', label: 'Integraciones', href: '/app/operations/pos/integrations' }, - { id: 'sync-status', label: 'Estado Sincronización', href: '/app/operations/pos/sync-status' }, - { id: 'transactions', label: 'Transacciones', href: '/app/operations/pos/transactions' } - ] - } - ]; - - // Add recipes for individual bakeries, hide for central - if (bakeryType === 'individual') { - baseItems.push({ - id: 'recipes', - label: 'Recetas', - href: '/app/operations/recipes', - icon: 'BookOpen', - children: [ - { id: 'active-recipes', label: 'Recetas Activas', href: '/app/operations/recipes/active-recipes' }, - { id: 'development', label: 'Desarrollo', href: '/app/operations/recipes/development' }, - { id: 'costing', label: 'Costeo', href: '/app/operations/recipes/costing' } - ] - }); - } - - return baseItems; - }; - - return ( -
-
-
- - -
-
- -
-
- -
-
-
- ); -}; - -export default OperationsLayout; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/layout/SettingsLayout.tsx b/fdev-ffrontend/src/components/layout/SettingsLayout.tsx deleted file mode 100644 index ea893c7b..00000000 --- a/fdev-ffrontend/src/components/layout/SettingsLayout.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import { Outlet } from 'react-router-dom'; -import { SecondaryNavigation } from '../navigation/SecondaryNavigation'; -import { Breadcrumbs } from '../navigation/Breadcrumbs'; -import { usePermissions } from '../../hooks/usePermissions'; - -const SettingsLayout: React.FC = () => { - const { hasRole } = usePermissions(); - - const getNavigationItems = () => { - const baseItems = [ - { - id: 'general', - label: 'General', - href: '/app/settings/general', - icon: 'Settings' - }, - { - id: 'account', - label: 'Cuenta', - href: '/app/settings/account', - icon: 'User' - } - ]; - - // Add admin-only items - if (hasRole('admin')) { - baseItems.unshift( - { - id: 'bakeries', - label: 'Panaderías', - href: '/app/settings/bakeries', - icon: 'Building' - }, - { - id: 'users', - label: 'Usuarios', - href: '/app/settings/users', - icon: 'Users' - } - ); - } - - return baseItems; - }; - - return ( -
-
-
- - -
-
- -
-
- -
-
-
- ); -}; - -export default SettingsLayout; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/navigation/Breadcrumbs.tsx b/fdev-ffrontend/src/components/navigation/Breadcrumbs.tsx deleted file mode 100644 index 7e8813c4..00000000 --- a/fdev-ffrontend/src/components/navigation/Breadcrumbs.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { ChevronRight, Home } from 'lucide-react'; - -interface BreadcrumbItem { - label: string; - href?: string; -} - -export const Breadcrumbs: React.FC = () => { - const location = useLocation(); - - const getBreadcrumbs = (): BreadcrumbItem[] => { - const pathSegments = location.pathname.split('/').filter(Boolean); - - // Remove 'app' from the beginning if present - if (pathSegments[0] === 'app') { - pathSegments.shift(); - } - - const breadcrumbs: BreadcrumbItem[] = [ - { label: 'Inicio', href: '/app/dashboard' } - ]; - - const segmentMap: Record = { - // Main sections - 'dashboard': 'Dashboard', - 'operations': 'Operaciones', - 'analytics': 'Analytics', - 'settings': 'Configuración', - - // Operations subsections - 'production': 'Producción', - 'orders': 'Pedidos', - 'inventory': 'Inventario', - 'sales': 'Ventas', - 'recipes': 'Recetas', - - // Operations sub-pages - 'schedule': 'Programación', - 'active-batches': 'Lotes Activos', - 'equipment': 'Equipamiento', - 'incoming': 'Entrantes', - 'in-progress': 'En Proceso', - 'supplier-orders': 'Proveedores', - 'stock-levels': 'Niveles Stock', - 'movements': 'Movimientos', - 'alerts': 'Alertas', - 'daily-sales': 'Ventas Diarias', - 'customer-orders': 'Pedidos Cliente', - 'pos-integration': 'Integración TPV', - 'active-recipes': 'Recetas Activas', - 'development': 'Desarrollo', - 'costing': 'Costeo', - - // Analytics subsections - 'forecasting': 'Predicciones', - 'sales-analytics': 'Análisis Ventas', - 'production-reports': 'Reportes Producción', - 'financial-reports': 'Reportes Financieros', - 'performance-kpis': 'KPIs', - 'ai-insights': 'Insights IA', - - // Settings subsections - 'general': 'General', - 'users': 'Usuarios', - 'bakeries': 'Panaderías', - 'account': 'Cuenta' - }; - - let currentPath = '/app'; - - pathSegments.forEach((segment, index) => { - currentPath += `/${segment}`; - const label = segmentMap[segment] || segment.charAt(0).toUpperCase() + segment.slice(1); - - // Don't make the last item clickable - const isLast = index === pathSegments.length - 1; - - breadcrumbs.push({ - label, - href: isLast ? undefined : currentPath - }); - }); - - return breadcrumbs; - }; - - const breadcrumbs = getBreadcrumbs(); - - if (breadcrumbs.length <= 1) { - return null; - } - - return ( - - ); -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/navigation/SecondaryNavigation.tsx b/fdev-ffrontend/src/components/navigation/SecondaryNavigation.tsx deleted file mode 100644 index b9ec52bc..00000000 --- a/fdev-ffrontend/src/components/navigation/SecondaryNavigation.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React, { useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { ChevronDown, ChevronRight } from 'lucide-react'; -import * as Icons from 'lucide-react'; - -interface NavigationChild { - id: string; - label: string; - href: string; -} - -interface NavigationItem { - id: string; - label: string; - href: string; - icon: string; - children?: NavigationChild[]; -} - -interface SecondaryNavigationProps { - items: NavigationItem[]; -} - -export const SecondaryNavigation: React.FC = ({ items }) => { - const location = useLocation(); - const [expandedItems, setExpandedItems] = useState>(new Set()); - - const toggleExpanded = (itemId: string) => { - const newExpanded = new Set(expandedItems); - if (newExpanded.has(itemId)) { - newExpanded.delete(itemId); - } else { - newExpanded.add(itemId); - } - setExpandedItems(newExpanded); - }; - - const isActive = (href: string): boolean => { - return location.pathname === href || location.pathname.startsWith(href + '/'); - }; - - const hasActiveChild = (children?: NavigationChild[]): boolean => { - if (!children) return false; - return children.some(child => isActive(child.href)); - }; - - // Auto-expand items with active children - React.useEffect(() => { - const itemsToExpand = new Set(expandedItems); - items.forEach(item => { - if (hasActiveChild(item.children)) { - itemsToExpand.add(item.id); - } - }); - setExpandedItems(itemsToExpand); - }, [location.pathname]); - - const getIcon = (iconName: string) => { - const IconComponent = (Icons as any)[iconName]; - return IconComponent || Icons.Circle; - }; - - return ( - - ); -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/navigation/TenantSelector.tsx b/fdev-ffrontend/src/components/navigation/TenantSelector.tsx deleted file mode 100644 index af6774ff..00000000 --- a/fdev-ffrontend/src/components/navigation/TenantSelector.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { ChevronDown, Building, Check } from 'lucide-react'; -import { useSelector, useDispatch } from 'react-redux'; -import { RootState } from '../../store'; -import { setCurrentTenant } from '../../store/slices/tenantSlice'; -import { useTenant } from '../../api/hooks/useTenant'; -import toast from 'react-hot-toast'; - -export const TenantSelector: React.FC = () => { - const dispatch = useDispatch(); - const { currentTenant } = useSelector((state: RootState) => state.tenant); - const { user } = useSelector((state: RootState) => state.auth); - const [isOpen, setIsOpen] = useState(false); - - const { - tenants, - getUserTenants, - isLoading, - error - } = useTenant(); - - useEffect(() => { - if (user) { - getUserTenants(); - } - }, [user, getUserTenants]); - - // Auto-select tenant based on localStorage or default to first one - useEffect(() => { - if (Array.isArray(tenants) && tenants.length > 0 && !currentTenant) { - const savedTenantId = localStorage.getItem('selectedTenantId'); - const tenantToSelect = savedTenantId - ? tenants.find(t => t.id === savedTenantId) || tenants[0] - : tenants[0]; - - console.log('🎯 Auto-selecting tenant:', tenantToSelect); - dispatch(setCurrentTenant(tenantToSelect)); - localStorage.setItem('selectedTenantId', tenantToSelect.id); - } - }, [tenants, currentTenant, dispatch]); - - const handleTenantChange = async (tenant: any) => { - try { - dispatch(setCurrentTenant(tenant)); - localStorage.setItem('selectedTenantId', tenant.id); - setIsOpen(false); - - toast.success(`Cambiado a ${tenant.name}`); - - // Force a page reload to update data with new tenant context - window.location.reload(); - } catch (error) { - toast.error('Error al cambiar de panadería'); - } - }; - - if (isLoading) { - return ( -
- Cargando panaderías... -
- ); - } - - // Show current tenant name even if there's only one - if (!Array.isArray(tenants) || tenants.length === 0) { - return ( -
- No hay panaderías disponibles -
- ); - } - - // If there's only one tenant, just show its name without dropdown - if (tenants.length === 1) { - const tenant = tenants[0]; - return ( -
- - {tenant.name} -
- ); - } - - return ( -
- - - {isOpen && ( - <> - {/* Backdrop */} -
setIsOpen(false)} - /> - - {/* Dropdown */} -
-
-

- Mis Panaderías -

-
- -
- {Array.isArray(tenants) ? tenants.map((tenant) => ( - - )) : ( -
- No hay panaderías disponibles -
- )} -
- -
- -
-
- - )} -
- ); -}; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/onboarding/SmartHistoricalDataImport.tsx b/fdev-ffrontend/src/components/onboarding/SmartHistoricalDataImport.tsx deleted file mode 100644 index 248a88a8..00000000 --- a/fdev-ffrontend/src/components/onboarding/SmartHistoricalDataImport.tsx +++ /dev/null @@ -1,891 +0,0 @@ -import React, { useState, useCallback } from 'react'; -import { - Upload, - Brain, - Check, - AlertTriangle, - Loader, - Store, - Factory, - Settings2, - Package, - Coffee, - Wheat, - Eye, - EyeOff, - CheckCircle2, - XCircle, - ArrowRight, - Lightbulb, - Building2, - Truck -} from 'lucide-react'; -import toast from 'react-hot-toast'; - -import { - FileValidationResult, - ProductSuggestionsResult, - OnboardingAnalysisResult, - InventorySuggestion, - BusinessModelAnalysis, - InventoryCreationResult, - SalesImportResult, - onboardingService -} from '../../api/services/onboarding.service'; - -interface SmartHistoricalDataImportProps { - tenantId: string; - onComplete: (result: SalesImportResult) => void; - onBack?: () => void; -} - -type ImportPhase = 'upload' | 'validation' | 'suggestions' | 'review' | 'creation' | 'import' | 'complete'; - -interface PhaseState { - phase: ImportPhase; - file?: File; - validationResult?: FileValidationResult; - suggestionsResult?: ProductSuggestionsResult; - reviewedSuggestions?: InventorySuggestion[]; - creationResult?: InventoryCreationResult; - importResult?: SalesImportResult; - error?: string; -} - -const SmartHistoricalDataImport: React.FC = ({ - tenantId, - onComplete, - onBack -}) => { - const [state, setState] = useState({ phase: 'upload' }); - const [isProcessing, setIsProcessing] = useState(false); - const [showAllSuggestions, setShowAllSuggestions] = useState(false); - - const handleFileUpload = useCallback(async (file: File) => { - setState(prev => ({ ...prev, file, phase: 'validation' })); - setIsProcessing(true); - - try { - // Step 1: Validate file and extract products - toast.loading('📋 Validando archivo...', { id: 'validation' }); - - const validationResult = await onboardingService.validateFileAndExtractProducts(tenantId, file); - - if (!validationResult.is_valid) { - throw new Error(`Archivo inválido: ${validationResult.validation_errors.map(e => e.message || e).join(', ')}`); - } - - toast.success(`¡Archivo válido! ${validationResult.unique_products} productos únicos encontrados`, { - id: 'validation' - }); - - setState(prev => ({ ...prev, validationResult, phase: 'suggestions' })); - - // Step 2: Generate AI suggestions - setTimeout(() => handleGenerateSuggestions(file, validationResult.product_list), 1000); - - } catch (error: any) { - toast.error('Error al validar el archivo', { id: 'validation' }); - setState(prev => ({ - ...prev, - error: error.message || 'Error de validación', - phase: 'upload' - })); - } finally { - setIsProcessing(false); - } - }, [tenantId]); - - const handleGenerateSuggestions = useCallback(async (file: File, productList: string[]) => { - setIsProcessing(true); - - try { - toast.loading('🧠 Generando sugerencias con IA...', { id: 'suggestions' }); - - const suggestionsResult = await onboardingService.generateInventorySuggestions(tenantId, file, productList); - - toast.success(`¡${suggestionsResult.total_products} productos clasificados! ${suggestionsResult.high_confidence_count} con alta confianza`, { - id: 'suggestions' - }); - - setState(prev => ({ - ...prev, - suggestionsResult, - reviewedSuggestions: suggestionsResult.suggestions.map(s => ({ - ...s, - user_approved: s.confidence_score >= 0.7 - })), - phase: 'review' - })); - - } catch (error: any) { - toast.error('Error al generar sugerencias', { id: 'suggestions' }); - setState(prev => ({ - ...prev, - error: error.message || 'Error en sugerencias de IA', - phase: 'validation' - })); - } finally { - setIsProcessing(false); - } - }, [tenantId]); - - const handleSuggestionUpdate = useCallback((suggestionId: string, updates: Partial) => { - setState(prev => ({ - ...prev, - reviewedSuggestions: prev.reviewedSuggestions?.map(s => - s.suggestion_id === suggestionId ? { ...s, ...updates } : s - ) - })); - }, []); - - const handleCreateInventory = useCallback(async () => { - if (!state.reviewedSuggestions) return; - - setState(prev => ({ ...prev, phase: 'creation' })); - setIsProcessing(true); - - try { - const approvedSuggestions = state.reviewedSuggestions.filter(s => s.user_approved); - - if (approvedSuggestions.length === 0) { - toast.error('Debes aprobar al menos un producto para continuar'); - setState(prev => ({ ...prev, phase: 'review' })); - setIsProcessing(false); - return; - } - - toast.loading(`Creando ${approvedSuggestions.length} productos en tu inventario...`, { id: 'creation' }); - - const creationResult = await onboardingService.createInventoryFromSuggestions( - tenantId, - approvedSuggestions - ); - - toast.success(`¡${creationResult.created_items.length} productos creados exitosamente!`, { - id: 'creation' - }); - - setState(prev => ({ ...prev, creationResult, phase: 'import' })); - - // Auto-proceed to final import - setTimeout(() => handleFinalImport(creationResult), 1500); - - } catch (error: any) { - toast.error('Error al crear productos en inventario', { id: 'creation' }); - setState(prev => ({ - ...prev, - error: error.message || 'Error al crear inventario', - phase: 'review' - })); - } finally { - setIsProcessing(false); - } - }, [state.reviewedSuggestions, tenantId]); - - const handleFinalImport = useCallback(async (creationResult?: InventoryCreationResult) => { - if (!state.file || !state.reviewedSuggestions) return; - - const currentCreationResult = creationResult || state.creationResult; - if (!currentCreationResult) return; - - setIsProcessing(true); - - try { - // Create mapping from product names to inventory IDs - const inventoryMapping: Record = {}; - - currentCreationResult.created_items.forEach(item => { - // Find the original suggestion that created this item - const suggestion = state.reviewedSuggestions!.find(s => - s.suggested_name === item.name || s.original_name === item.original_name - ); - - if (suggestion) { - inventoryMapping[suggestion.original_name] = item.id; - } - }); - - toast.loading('Importando datos históricos con inventario...', { id: 'import' }); - - const importResult = await onboardingService.importSalesWithInventory( - tenantId, - state.file, - inventoryMapping - ); - - toast.success( - `¡Importación completada! ${importResult.successful_imports} registros importados`, - { id: 'import' } - ); - - setState(prev => ({ ...prev, importResult, phase: 'complete' })); - - // Complete the process - setTimeout(() => onComplete(importResult), 2000); - - } catch (error: any) { - toast.error('Error en importación final', { id: 'import' }); - setState(prev => ({ - ...prev, - error: error.message || 'Error en importación final', - phase: 'creation' - })); - } finally { - setIsProcessing(false); - } - }, [state.file, state.reviewedSuggestions, state.creationResult, tenantId, onComplete]); - - const renderBusinessModelInsight = (analysis: BusinessModelAnalysis) => { - const modelConfig = { - individual_bakery: { - icon: Factory, - title: 'Panadería Individual', - description: 'Producción completa desde ingredientes básicos (harina, levadura, etc.)', - color: 'blue', - bgColor: 'bg-blue-50', - borderColor: 'border-blue-200', - textColor: 'text-blue-900', - businessType: 'bakery' - }, - central_baker_satellite: { - icon: Truck, - title: 'Punto de Venta - Obrador Central', - description: 'Recibe productos semi-elaborados y los finaliza (horneado, decoración)', - color: 'amber', - bgColor: 'bg-amber-50', - borderColor: 'border-amber-200', - textColor: 'text-amber-900', - businessType: 'bakery' - }, - retail_bakery: { - icon: Store, - title: 'Panadería de Distribución', - description: 'Vende productos terminados de proveedores externos', - color: 'green', - bgColor: 'bg-green-50', - borderColor: 'border-green-200', - textColor: 'text-green-900', - businessType: 'bakery' - }, - hybrid_bakery: { - icon: Settings2, - title: 'Modelo Mixto', - description: 'Combina producción propia con productos de proveedores', - color: 'purple', - bgColor: 'bg-purple-50', - borderColor: 'border-purple-200', - textColor: 'text-purple-900', - businessType: 'bakery' - }, - coffee_shop_individual: { - icon: Coffee, - title: 'Cafetería Individual', - description: 'Servicio de bebidas y comida ligera con preparación in-situ', - color: 'amber', - bgColor: 'bg-amber-50', - borderColor: 'border-amber-200', - textColor: 'text-amber-900', - businessType: 'coffee_shop' - }, - coffee_shop_chain: { - icon: Building2, - title: 'Cafetería en Cadena', - description: 'Múltiples ubicaciones con productos estandarizados', - color: 'indigo', - bgColor: 'bg-indigo-50', - borderColor: 'border-indigo-200', - textColor: 'text-indigo-900', - businessType: 'coffee_shop' - }, - // Legacy fallbacks - production: { - icon: Factory, - title: 'Panadería de Producción', - description: 'Produces items from raw ingredients', - color: 'blue', - bgColor: 'bg-blue-50', - borderColor: 'border-blue-200', - textColor: 'text-blue-900', - businessType: 'bakery' - }, - retail: { - icon: Store, - title: 'Panadería de Distribución', - description: 'Sells finished products from suppliers', - color: 'green', - bgColor: 'bg-green-50', - borderColor: 'border-green-200', - textColor: 'text-green-900', - businessType: 'bakery' - }, - hybrid: { - icon: Settings2, - title: 'Modelo Híbrido', - description: 'Both produces and distributes products', - color: 'purple', - bgColor: 'bg-purple-50', - borderColor: 'border-purple-200', - textColor: 'text-purple-900', - businessType: 'bakery' - } - }; - - // Provide fallback if analysis.model is not in modelConfig - const config = modelConfig[analysis.model as keyof typeof modelConfig] || modelConfig.hybrid_bakery; - const IconComponent = config.icon; - - return ( -
-
-
- -
-
-
-

{config.title}

- - {Math.round(analysis.confidence * 100)}% confianza - -
-

{config.description}

- -
-
- - - {analysis.ingredient_count} ingredientes - -
-
- - - {analysis.finished_product_count} productos finales - -
-
- - {/* Enhanced business intelligence insights if available */} - {config.businessType === 'coffee_shop' && ( -
-
- - - Negocio de Cafetería Detectado - -
-

- Hemos detectado que tu negocio se enfoca principalmente en bebidas y comida ligera. - El sistema se optimizará para gestión de inventario de cafetería. -

-
- )} - - {analysis.recommendations.length > 0 && ( -
-

- Recomendaciones personalizadas: -

-
    - {analysis.recommendations.slice(0, 2).map((rec, idx) => ( -
  • - - {rec} -
  • - ))} -
-
- )} -
-
-
- ); - }; - - const renderSuggestionCard = (suggestion: InventorySuggestion) => { - const isHighConfidence = suggestion.confidence_score >= 0.7; - const isMediumConfidence = suggestion.confidence_score >= 0.4; - - return ( -
-
-
-
- - -
-

{suggestion.suggested_name}

- {suggestion.original_name !== suggestion.suggested_name && ( -

"{suggestion.original_name}"

- )} -
-
-
- -
-
- {Math.round(suggestion.confidence_score * 100)}% confianza -
-
-
- -
-
- Tipo: - - {suggestion.product_type === 'ingredient' ? 'Ingrediente' : 'Producto Final'} - -
-
- Categoría: - {suggestion.category} -
-
- Unidad: - {suggestion.unit_of_measure} -
- {suggestion.estimated_shelf_life_days && ( -
- Duración: - {suggestion.estimated_shelf_life_days} días -
- )} -
- - {(suggestion.requires_refrigeration || suggestion.requires_freezing || suggestion.is_seasonal) && ( -
- {suggestion.requires_refrigeration && ( - - ❄️ Refrigeración - - )} - {suggestion.requires_freezing && ( - - 🧊 Congelación - - )} - {suggestion.is_seasonal && ( - - 🍂 Estacional - - )} -
- )} - - {!isHighConfidence && suggestion.notes && ( -
- 💡 {suggestion.notes} -
- )} -
- ); - }; - - // Main render logic based on current phase - switch (state.phase) { - case 'upload': - return ( -
-
-
- -
-

- Importación Inteligente de Datos -

-

- Nuestra IA analizará tus datos históricos y creará automáticamente tu inventario -

-
- -
-

- 🚀 ¿Cómo funciona la magia? -

-
-
-
- -
-
1. Subes tu archivo
-
CSV, Excel o JSON
-
-
-
- -
-
2. IA analiza productos
-
Clasificación inteligente
-
-
-
- -
-
3. Inventario listo
-
Con categorías y detalles
-
-
-
- -
- - - { - const file = e.target.files?.[0]; - if (file) { - if (file.size > 10 * 1024 * 1024) { - toast.error('El archivo es demasiado grande. Máximo 10MB.'); - return; - } - handleFileUpload(file); - } - }} - className="hidden" - disabled={isProcessing} - /> -
- - {state.error && ( -
-
- -
-

Error

-

{state.error}

-
-
-
- )} -
- ); - - case 'validation': - return ( -
-
- -
-

- 📋 Validando archivo... -

-

- Verificando formato y extrayendo productos únicos -

-
-
- Archivo: - {state.file?.name} -
-
-
-
-
Paso 1 de 4: Validación
-
-
- ); - - case 'suggestions': - return ( -
-
- -
-

- 🧠 Generando sugerencias con IA... -

-

- Clasificando productos y analizando tu modelo de negocio -

-
- {state.validationResult && ( -
-
- - - {state.validationResult.unique_products} productos únicos encontrados - -
-
- )} -
-
-
-
Paso 2 de 4: Clasificación IA
-
-
- ); - - case 'review': - if (!state.suggestionsResult) return null; - - const { suggestionsResult, reviewedSuggestions } = state; - const approvedCount = reviewedSuggestions?.filter(s => s.user_approved).length || 0; - const highConfidenceCount = reviewedSuggestions?.filter(s => s.confidence_score >= 0.7).length || 0; - const visibleSuggestions = showAllSuggestions - ? reviewedSuggestions - : reviewedSuggestions?.slice(0, 6); - - return ( -
-
-
- -
-

- ¡Análisis Completado! 🎉 -

-

- Hemos encontrado {suggestionsResult.total_products} productos y - sugerimos {approvedCount} para tu inventario -

-
- ⚡ Procesado en {suggestionsResult.processing_time_seconds.toFixed(1)}s -
-
- - {renderBusinessModelInsight(suggestionsResult.business_model_analysis)} - -
-
-
-

- Productos Sugeridos para tu Inventario -

-

- {highConfidenceCount} con alta confianza • {approvedCount} pre-aprobados -

-
- -
- - - {(reviewedSuggestions?.length || 0) > 6 && ( - - )} -
-
- -
- {visibleSuggestions?.map(renderSuggestionCard)} -
- - {state.validationResult?.validation_warnings && state.validationResult.validation_warnings.length > 0 && ( -
-
- -
-

Advertencias de Validación

-
    - {state.validationResult.validation_warnings.map((warning, idx) => ( -
  • • {warning.message || warning}
  • - ))} -
-
-
-
- )} - -
- {onBack && ( - - )} - - -
-
-
- ); - - case 'creation': - case 'import': - const isCreating = state.phase === 'creation'; - const stepNumber = isCreating ? 3 : 4; - const stepProgress = isCreating ? 75 : 90; - - return ( -
-
- {isCreating ? ( - - ) : ( - - )} -
-

- {isCreating ? '📦 Creando productos en tu inventario...' : '📊 Importando datos históricos...'} -

-

- {isCreating - ? 'Configurando cada producto con sus detalles específicos' - : 'Vinculando tus ventas históricas con el nuevo inventario' - } -

- -
- {state.creationResult && ( -
-
- - - {state.creationResult.created_items.length} productos creados - -
-
- )} - -
-
-
-
-

- {isCreating ? 'Creando inventario...' : 'Procesando importación final...'} -

- Paso {stepNumber} de 4 -
-
-
- ); - - case 'complete': - if (!state.importResult || !state.creationResult) return null; - - return ( -
-
- -
- -

- ¡Importación Completada! 🎉 -

-

- Tu inventario inteligente está listo -

- -
-
-
-
- -
-
- {state.creationResult.created_items.length} -
-
Productos en inventario
-
- -
-
- -
-
- {state.importResult.successful_imports} -
-
Registros históricos
-
-
- -
-

- ✨ Tu IA está lista para predecir la demanda con precisión -

-
-
-
- ); - - default: - return null; - } -}; - -export default SmartHistoricalDataImport; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/pos/POSConfigurationCard.tsx b/fdev-ffrontend/src/components/pos/POSConfigurationCard.tsx deleted file mode 100644 index bff5e64d..00000000 --- a/fdev-ffrontend/src/components/pos/POSConfigurationCard.tsx +++ /dev/null @@ -1,356 +0,0 @@ -import React, { useState } from 'react'; -import { - Edit, - RefreshCw, - Globe, - Activity, - CheckCircle, - AlertTriangle, - Clock, - Settings, - MoreVertical, - Trash2, - Power, - Zap -} from 'lucide-react'; - -import Button from '../ui/Button'; -import Card from '../ui/Card'; -import LoadingSpinner from '../ui/LoadingSpinner'; - -interface POSConfiguration { - id: string; - pos_system: string; - provider_name: string; - is_active: boolean; - is_connected: boolean; - environment: string; - location_id?: string; - merchant_id?: string; - sync_enabled: boolean; - sync_interval_minutes: string; - auto_sync_products: boolean; - auto_sync_transactions: boolean; - last_sync_at?: string; - last_successful_sync_at?: string; - last_sync_status?: string; - health_status: string; - created_at: string; - updated_at: string; -} - -interface POSConfigurationCardProps { - configuration: POSConfiguration; - onEdit: (config: POSConfiguration) => void; - onTriggerSync: (configId: string) => Promise; - onTestConnection: (configId: string) => Promise; - onToggleActive?: (configId: string, active: boolean) => Promise; - onDelete?: (configId: string) => Promise; -} - -const POSConfigurationCard: React.FC = ({ - configuration, - onEdit, - onTriggerSync, - onTestConnection, - onToggleActive, - onDelete -}) => { - const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isSyncing, setIsSyncing] = useState(false); - const [isTesting, setIsTesting] = useState(false); - const [isToggling, setIsToggling] = useState(false); - - const getPOSSystemIcon = (system: string) => { - switch (system) { - case 'square': return '⬜'; - case 'toast': return '🍞'; - case 'lightspeed': return '⚡'; - default: return '💳'; - } - }; - - const getStatusColor = (status: string) => { - switch (status) { - case 'healthy': return 'text-green-600'; - case 'unhealthy': return 'text-red-600'; - case 'warning': return 'text-yellow-600'; - default: return 'text-gray-600'; - } - }; - - const getStatusIcon = (status: string) => { - switch (status) { - case 'healthy': return CheckCircle; - case 'unhealthy': return AlertTriangle; - case 'warning': return Clock; - default: return Activity; - } - }; - - const getConnectionStatusColor = (isConnected: boolean) => { - return isConnected ? 'text-green-600' : 'text-red-600'; - }; - - const getSyncStatusColor = (status?: string) => { - switch (status) { - case 'success': return 'text-green-600'; - case 'failed': return 'text-red-600'; - case 'partial': return 'text-yellow-600'; - default: return 'text-gray-600'; - } - }; - - const formatLastSync = (timestamp?: string) => { - if (!timestamp) return 'Never'; - - const date = new Date(timestamp); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / (1000 * 60)); - - if (diffMins < 1) return 'Just now'; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffMins < 1440) return `${Math.floor(diffMins / 60)}h ago`; - return `${Math.floor(diffMins / 1440)}d ago`; - }; - - const handleSync = async () => { - setIsSyncing(true); - try { - await onTriggerSync(configuration.id); - } finally { - setIsSyncing(false); - } - }; - - const handleTestConnection = async () => { - setIsTesting(true); - try { - await onTestConnection(configuration.id); - } finally { - setIsTesting(false); - } - }; - - const handleToggleActive = async () => { - if (!onToggleActive) return; - - setIsToggling(true); - try { - await onToggleActive(configuration.id, !configuration.is_active); - } finally { - setIsToggling(false); - } - }; - - const StatusIcon = getStatusIcon(configuration.health_status); - - return ( - - {/* Header */} -
-
-
- {getPOSSystemIcon(configuration.pos_system)} -
-
-

- {configuration.provider_name} -

-

- {configuration.pos_system} • {configuration.environment} -

-
-
- -
- - - {isMenuOpen && ( -
-
- - - {onToggleActive && ( - - )} - - {onDelete && ( - - )} -
-
- )} -
-
- - {/* Status Indicators */} -
-
-
- - {configuration.health_status} -
-

Health

-
- -
-
- - - {configuration.is_connected ? 'Connected' : 'Disconnected'} - -
-

Connection

-
- -
-
- - - {configuration.last_sync_status || 'Unknown'} - -
-

Last Sync

-
-
- - {/* Configuration Details */} -
- {configuration.location_id && ( -
- Location ID: - {configuration.location_id} -
- )} - -
- Sync Interval: - {configuration.sync_interval_minutes}m -
- -
- Last Sync: - {formatLastSync(configuration.last_sync_at)} -
- -
- Auto Sync: -
- {configuration.auto_sync_transactions && ( - Transactions - )} - {configuration.auto_sync_products && ( - Products - )} -
-
-
- - {/* Status Badge */} -
-
- - {configuration.is_active ? 'Active' : 'Inactive'} - - - {configuration.sync_enabled && ( - - Sync Enabled - - )} -
-
- - {/* Action Buttons */} -
- - - - - -
- - {/* Click overlay to close menu */} - {isMenuOpen && ( -
setIsMenuOpen(false)} - /> - )} - - ); -}; - -export default POSConfigurationCard; \ No newline at end of file diff --git a/fdev-ffrontend/src/components/pos/POSConfigurationForm.tsx b/fdev-ffrontend/src/components/pos/POSConfigurationForm.tsx deleted file mode 100644 index 0e9f5320..00000000 --- a/fdev-ffrontend/src/components/pos/POSConfigurationForm.tsx +++ /dev/null @@ -1,595 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { - X, - Zap, - Settings, - Globe, - Key, - Webhook, - RefreshCw, - AlertTriangle, - CheckCircle, - Clock, - Database -} from 'lucide-react'; - -import Button from '../ui/Button'; -import LoadingSpinner from '../ui/LoadingSpinner'; - -interface POSConfiguration { - id?: string; - pos_system: string; - provider_name: string; - is_active: boolean; - is_connected: boolean; - environment: string; - location_id?: string; - merchant_id?: string; - sync_enabled: boolean; - sync_interval_minutes: string; - auto_sync_products: boolean; - auto_sync_transactions: boolean; - webhook_url?: string; - notes?: string; -} - -interface POSConfigurationFormProps { - configuration?: POSConfiguration | null; - isOpen: boolean; - isCreating?: boolean; - onSubmit: (data: POSConfiguration) => Promise; - onClose: () => void; -} - -interface FormData extends POSConfiguration { - // Credentials (these won't be in the existing config for security) - api_key?: string; - api_secret?: string; - access_token?: string; - application_id?: string; - webhook_secret?: string; -} - -const SUPPORTED_POS_SYSTEMS = [ - { - id: 'square', - name: 'Square POS', - description: 'Square Point of Sale system', - logo: '⬜', - fields: ['application_id', 'access_token', 'webhook_secret'] - }, - { - id: 'toast', - name: 'Toast POS', - description: 'Toast restaurant POS system', - logo: '🍞', - fields: ['api_key', 'api_secret', 'webhook_secret'] - }, - { - id: 'lightspeed', - name: 'Lightspeed Restaurant', - description: 'Lightspeed restaurant management system', - logo: '⚡', - fields: ['api_key', 'api_secret', 'cluster_id'] - } -]; - -const POSConfigurationForm: React.FC = ({ - configuration, - isOpen, - isCreating = false, - onSubmit, - onClose -}) => { - const [formData, setFormData] = useState({ - pos_system: '', - provider_name: '', - is_active: true, - is_connected: false, - environment: 'sandbox', - sync_enabled: true, - sync_interval_minutes: '5', - auto_sync_products: true, - auto_sync_transactions: true, - ...configuration - }); - - const [isLoading, setIsLoading] = useState(false); - const [errors, setErrors] = useState>({}); - const [testingConnection, setTestingConnection] = useState(false); - const [connectionStatus, setConnectionStatus] = useState<'idle' | 'success' | 'error'>('idle'); - - useEffect(() => { - if (configuration) { - setFormData({ - ...formData, - ...configuration - }); - } - }, [configuration]); - - const selectedPOSSystem = SUPPORTED_POS_SYSTEMS.find(sys => sys.id === formData.pos_system); - - const validateForm = (): boolean => { - const newErrors: Record = {}; - - if (!formData.pos_system) { - newErrors.pos_system = 'POS system is required'; - } - - if (!formData.provider_name.trim()) { - newErrors.provider_name = 'Provider name is required'; - } - - if (selectedPOSSystem?.fields.includes('api_key') && !formData.api_key?.trim()) { - newErrors.api_key = 'API Key is required'; - } - - if (selectedPOSSystem?.fields.includes('access_token') && !formData.access_token?.trim()) { - newErrors.access_token = 'Access Token is required'; - } - - if (selectedPOSSystem?.fields.includes('application_id') && !formData.application_id?.trim()) { - newErrors.application_id = 'Application ID is required'; - } - - const syncInterval = parseInt(formData.sync_interval_minutes); - if (isNaN(syncInterval) || syncInterval < 1 || syncInterval > 1440) { - newErrors.sync_interval_minutes = 'Sync interval must be between 1 and 1440 minutes'; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!validateForm()) { - return; - } - - setIsLoading(true); - try { - await onSubmit(formData); - onClose(); - } catch (error) { - console.error('Error submitting form:', error); - } finally { - setIsLoading(false); - } - }; - - const handleTestConnection = async () => { - if (!validateForm()) { - return; - } - - setTestingConnection(true); - setConnectionStatus('idle'); - - try { - // TODO: Implement connection test API call - await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate API call - setConnectionStatus('success'); - } catch (error) { - setConnectionStatus('error'); - } finally { - setTestingConnection(false); - } - }; - - const handleInputChange = (field: keyof FormData, value: any) => { - setFormData(prev => ({ - ...prev, - [field]: value - })); - - // Clear error when user starts typing - if (errors[field]) { - setErrors(prev => ({ - ...prev, - [field]: '' - })); - } - }; - - const handlePOSSystemChange = (posSystem: string) => { - const system = SUPPORTED_POS_SYSTEMS.find(sys => sys.id === posSystem); - setFormData(prev => ({ - ...prev, - pos_system: posSystem, - provider_name: system?.name || '', - // Clear credentials when changing systems - api_key: '', - api_secret: '', - access_token: '', - application_id: '', - webhook_secret: '' - })); - setConnectionStatus('idle'); - }; - - if (!isOpen) return null; - - return ( -
-
- {/* Header */} -
-
- -

- {isCreating ? 'Add POS Integration' : 'Edit POS Configuration'} -

-
- -
- -
- {/* POS System Selection */} -
-

- - POS System Configuration -

- -
- {SUPPORTED_POS_SYSTEMS.map((system) => ( - - ))} -
- {errors.pos_system && ( -

{errors.pos_system}

- )} -
- - {/* Basic Configuration */} -
-
- - handleInputChange('provider_name', e.target.value)} - className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ - errors.provider_name ? 'border-red-500' : 'border-gray-300' - }`} - placeholder="e.g., Main Store Square POS" - /> - {errors.provider_name && ( -

{errors.provider_name}

- )} -
- -
- - -
- -
- - handleInputChange('location_id', e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Optional location identifier" - /> -
- -
- - handleInputChange('merchant_id', e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Optional merchant identifier" - /> -
-
- - {/* API Credentials */} - {selectedPOSSystem && ( -
-

- - API Credentials -

- -
-
- -

- Credentials are encrypted and stored securely. Never share your API keys. -

-
-
- -
- {selectedPOSSystem.fields.includes('application_id') && ( -
- - handleInputChange('application_id', e.target.value)} - className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ - errors.application_id ? 'border-red-500' : 'border-gray-300' - }`} - placeholder="Square Application ID" - /> - {errors.application_id && ( -

{errors.application_id}

- )} -
- )} - - {selectedPOSSystem.fields.includes('access_token') && ( -
- - handleInputChange('access_token', e.target.value)} - className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ - errors.access_token ? 'border-red-500' : 'border-gray-300' - }`} - placeholder="Square Access Token" - /> - {errors.access_token && ( -

{errors.access_token}

- )} -
- )} - - {selectedPOSSystem.fields.includes('api_key') && ( -
- - handleInputChange('api_key', e.target.value)} - className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ - errors.api_key ? 'border-red-500' : 'border-gray-300' - }`} - placeholder="API Key" - /> - {errors.api_key && ( -

{errors.api_key}

- )} -
- )} - - {selectedPOSSystem.fields.includes('api_secret') && ( -
- - handleInputChange('api_secret', e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="API Secret" - /> -
- )} - - {selectedPOSSystem.fields.includes('webhook_secret') && ( -
- - handleInputChange('webhook_secret', e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Webhook verification secret" - /> -
- )} -
- - {/* Test Connection */} -
- - - {connectionStatus === 'success' && ( -
- - Connection successful -
- )} - - {connectionStatus === 'error' && ( -
- - Connection failed -
- )} -
-
- )} - - {/* Sync Configuration */} -
-

- - Synchronization Settings -

- -
-
- - handleInputChange('sync_interval_minutes', e.target.value)} - className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ - errors.sync_interval_minutes ? 'border-red-500' : 'border-gray-300' - }`} - /> - {errors.sync_interval_minutes && ( -

{errors.sync_interval_minutes}

- )} -
- -
- - - - - - - -
-
-
- - {/* Notes */} -
- -