From 9e827c50f9ca4a7d20784067e39f285a966cba85 Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Fri, 15 May 2026 00:48:18 +0200 Subject: [PATCH] Initial commit --- .gitignore | 28 + CLAUDE.md | 80 + README.md | 73 + eslint.config.js | 22 + index.html | 13 + package-lock.json | 3942 ++++++++++++++++++++ package.json | 41 + public/favicon.svg | 1 + public/icons.svg | 24 + src/App.css | 184 + src/App.tsx | 55 + src/assets/hero.png | Bin 0 -> 13057 bytes src/assets/react.svg | 1 + src/assets/vite.svg | 1 + src/components/layout/AppShell.tsx | 527 +++ src/components/layout/index.ts | 1 + src/components/ui/AppErrorBoundary.tsx | 45 + src/components/ui/EmptyState.tsx | 29 + src/components/ui/ErrorState.tsx | 19 + src/components/ui/LoadingPage.tsx | 21 + src/components/ui/PageContainer.tsx | 19 + src/components/ui/SectionContainer.tsx | 29 + src/components/ui/index.ts | 6 + src/domain/enums.ts | 65 + src/domain/futureSignal.ts | 33 + src/domain/index.ts | 5 + src/domain/match.ts | 53 + src/domain/need.ts | 65 + src/domain/property.ts | 73 + src/index.css | 4 + src/lib/theme.ts | 108 + src/main.tsx | 32 + src/mock-data/futureSignals.ts | 84 + src/mock-data/index.ts | 4 + src/mock-data/matches.ts | 141 + src/mock-data/needs.ts | 76 + src/mock-data/properties.ts | 192 + src/pages/Home.tsx | 43 + src/pages/demand/AISearch.tsx | 338 ++ src/pages/demand/Compare.tsx | 320 ++ src/pages/demand/Results.tsx | 476 +++ src/pages/demand/Shortlists.tsx | 174 + src/pages/ops/AIMonitoring.tsx | 280 ++ src/pages/ops/Governance.tsx | 311 ++ src/pages/ops/ReviewQueue.tsx | 380 ++ src/pages/supply/DataQuality.tsx | 309 ++ src/pages/supply/FutureAvailability.tsx | 237 ++ src/pages/supply/MatchCenter.tsx | 283 ++ src/pages/supply/Properties.tsx | 374 ++ src/pages/supply/SupplyDashboard.tsx | 283 ++ src/provider/IFutureSignalProvider.ts | 16 + src/provider/IMatchProvider.ts | 18 + src/provider/INeedProvider.ts | 16 + src/provider/IPropertyProvider.ts | 19 + src/provider/MockupFutureSignalProvider.ts | 27 + src/provider/MockupMatchProvider.ts | 31 + src/provider/MockupNeedProvider.ts | 32 + src/provider/MockupPropertyProvider.ts | 35 + src/services/aiService.ts | 57 + src/services/futureSignalService.ts | 25 + src/services/governanceService.ts | 41 + src/services/matchService.ts | 29 + src/services/needService.ts | 29 + src/services/propertyService.ts | 30 + src/services/types.ts | 15 + src/stores/compareStore.ts | 27 + src/stores/layoutStore.ts | 22 + src/stores/sessionStore.ts | 38 + tsconfig.app.json | 25 + tsconfig.json | 7 + tsconfig.node.json | 24 + vite.config.ts | 10 + 72 files changed, 10477 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/favicon.svg create mode 100644 public/icons.svg create mode 100644 src/App.css create mode 100644 src/App.tsx create mode 100644 src/assets/hero.png create mode 100644 src/assets/react.svg create mode 100644 src/assets/vite.svg create mode 100644 src/components/layout/AppShell.tsx create mode 100644 src/components/layout/index.ts create mode 100644 src/components/ui/AppErrorBoundary.tsx create mode 100644 src/components/ui/EmptyState.tsx create mode 100644 src/components/ui/ErrorState.tsx create mode 100644 src/components/ui/LoadingPage.tsx create mode 100644 src/components/ui/PageContainer.tsx create mode 100644 src/components/ui/SectionContainer.tsx create mode 100644 src/components/ui/index.ts create mode 100644 src/domain/enums.ts create mode 100644 src/domain/futureSignal.ts create mode 100644 src/domain/index.ts create mode 100644 src/domain/match.ts create mode 100644 src/domain/need.ts create mode 100644 src/domain/property.ts create mode 100644 src/index.css create mode 100644 src/lib/theme.ts create mode 100644 src/main.tsx create mode 100644 src/mock-data/futureSignals.ts create mode 100644 src/mock-data/index.ts create mode 100644 src/mock-data/matches.ts create mode 100644 src/mock-data/needs.ts create mode 100644 src/mock-data/properties.ts create mode 100644 src/pages/Home.tsx create mode 100644 src/pages/demand/AISearch.tsx create mode 100644 src/pages/demand/Compare.tsx create mode 100644 src/pages/demand/Results.tsx create mode 100644 src/pages/demand/Shortlists.tsx create mode 100644 src/pages/ops/AIMonitoring.tsx create mode 100644 src/pages/ops/Governance.tsx create mode 100644 src/pages/ops/ReviewQueue.tsx create mode 100644 src/pages/supply/DataQuality.tsx create mode 100644 src/pages/supply/FutureAvailability.tsx create mode 100644 src/pages/supply/MatchCenter.tsx create mode 100644 src/pages/supply/Properties.tsx create mode 100644 src/pages/supply/SupplyDashboard.tsx create mode 100644 src/provider/IFutureSignalProvider.ts create mode 100644 src/provider/IMatchProvider.ts create mode 100644 src/provider/INeedProvider.ts create mode 100644 src/provider/IPropertyProvider.ts create mode 100644 src/provider/MockupFutureSignalProvider.ts create mode 100644 src/provider/MockupMatchProvider.ts create mode 100644 src/provider/MockupNeedProvider.ts create mode 100644 src/provider/MockupPropertyProvider.ts create mode 100644 src/services/aiService.ts create mode 100644 src/services/futureSignalService.ts create mode 100644 src/services/governanceService.ts create mode 100644 src/services/matchService.ts create mode 100644 src/services/needService.ts create mode 100644 src/services/propertyService.ts create mode 100644 src/services/types.ts create mode 100644 src/stores/compareStore.ts create mode 100644 src/stores/layoutStore.ts create mode 100644 src/stores/sessionStore.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5b8580 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Windows system files +desktop.ini +Thumbs.db + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..01a1e3b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,80 @@ +# property-match — Development Guidelines + +## Stack + +- **Vite 8** + **React 19** + **TypeScript 6** +- **MUI v9** (`@mui/material`) — primary component library +- **Tailwind CSS v4** — utility classes via `@tailwindcss/vite` (no `tailwind.config.js`) +- **React Router v7** — import from `react-router`, not `react-router-dom` + +## Components + +Always reach for an existing MUI component before writing a custom one. Check the [MUI component list](https://mui.com/material-ui/all-components/) first. Only build a custom component when MUI has no equivalent or the required behavior diverges significantly from what MUI provides. + +## Styling + +Use Tailwind utility classes for all layout and styling. Do not write plain CSS rules or add styles to `.css` files. The only CSS file is `src/index.css`, which holds the Tailwind layer imports — do not add project styles there. + + +## Providers + +All data access and data actions live in `src/provider/`. + +### Naming + +| Rule | Example | +|------|---------| +| Every provider file/class is suffixed `Provider` | `PropertyProvider`, `UserProvider` | +| Every provider backed by mock data is also prefixed `Mockup` | `MockupPropertyProvider`, `MockupUserProvider` | + +### Interface pattern + +Define a TypeScript interface for each provider so the mockup and the real implementation are interchangeable: +Every Interface should be prefixed with a capitalized I. + +```ts +// src/provider/IPropertyProvider.ts +export interface PropertyProvider { + getAll(): Promise + getById(id: string): Promise + create(data: CreatePropertyInput): Promise + update(id: string, data: UpdatePropertyInput): Promise + remove(id: string): Promise +} +``` + +### Async methods + +Every method in a provider must be `async` and return a `Promise`, even in the mockup. This ensures the real provider can be swapped in without changing any call sites. + +```ts +// src/provider/MockupPropertyProvider.ts +import type { PropertyProvider } from './PropertyProvider' + +const properties: Property[] = [ /* seed data */ ] + +export const MockupPropertyProvider: PropertyProvider = { + async getAll() { + return [...properties] + }, + async getById(id) { + return properties.find(p => p.id === id) ?? null + }, + async create(data) { + const next: Property = { id: crypto.randomUUID(), ...data } + properties.push(next) + return next + }, + async update(id, data) { + const idx = properties.findIndex(p => p.id === id) + properties[idx] = { ...properties[idx], ...data } + return properties[idx] + }, + async remove(id) { + const idx = properties.findIndex(p => p.id === id) + properties.splice(idx, 1) + }, +} +``` + +Swap to a real implementation by replacing `MockupPropertyProvider` with a provider that calls an API — no other code changes required. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..ef614d2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..b0e5ae0 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Property Match + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9aa405f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3942 @@ +{ + "name": "property-match", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "property-match", + "version": "0.0.0", + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^9.0.1", + "@mui/material": "^9.0.1", + "@tanstack/react-query": "^5.75.2", + "lucide-react": "^0.511.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-router": "^7.15.0", + "zod": "^3.25.17", + "zustand": "^5.0.5" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.3.0", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "tailwindcss": "^4.3.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/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/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@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-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "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.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "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/eslint-utils/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-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.0.1.tgz", + "integrity": "sha512-GzamIIhZ1bH77dq7eKaeyRgJdkypsxin4jBFq2EMs4lBWRR0LFO1CSVMsoebn/VvjcNrnrOrjy48MkrkQUK2iw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.0.1.tgz", + "integrity": "sha512-5PRpQjVLTNLyV/2J9J53Yz4R0tVbodG0BQDN2zQI1QBG1OPYM25ar+4N20eyFOfJT6zKglLzsnU70+zdVLaTkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^9.0.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.0.1.tgz", + "integrity": "sha512-voyCpeUxcSWLN7KPZuq0pGCIt726T9K6kiVM3XUcywZDAlZSarLHaUxJVQpospbjjOzN53hwyjo8s6KoWl6utw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/core-downloads-tracker": "^9.0.1", + "@mui/system": "^9.0.1", + "@mui/types": "^9.0.0", + "@mui/utils": "^9.0.1", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.4", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^9.0.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.0.1.tgz", + "integrity": "sha512-pSIGq4Yw749KHEwlkYZWVERgHgwJELP6ODtBNUfV8V4oIb5H+h7IQDFXuk/b2oQccODK1enJAtiEzlgLZmq+8g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/utils": "^9.0.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.0.0.tgz", + "integrity": "sha512-9RLGdX4Jg0aQPRuvqh/OLzYSPlgd5zyEw5/1HIRfdavSiOd03WtUaGZH9/w1RoTYuRKwpgy0hpIFaMHIqPVIWg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.0.1.tgz", + "integrity": "sha512-WvlioaLxk6ewUIOfh0StxUvOPDS1mCfzaulcudsL1brZNXuh0N9FMk7RpH7ImJKjEz412SEy/V/yvqmtxbqxCQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/private-theming": "^9.0.1", + "@mui/styled-engine": "^9.0.0", + "@mui/types": "^9.0.0", + "@mui/utils": "^9.0.1", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.0.0.tgz", + "integrity": "sha512-i1cuFCAWN44b3AJWO7mh7tuh1sqbQSeVr/94oG0TX5uXivac8XalgE4/6fQZcmGZigzbQ35IXxj/4jLpRIBYZg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.0.1.tgz", + "integrity": "sha512-f3UO3jNN1pYg5zxqXC81Bvv8hx5ACcYc0387382ZI7M5ono1heIwHYLrKsz85myguWdeVKPRZGmDdynWUBjK2g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/types": "^9.0.0", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", + "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", + "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", + "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", + "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz", + "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.10.tgz", + "integrity": "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "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.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "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": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.3", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "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/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "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/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/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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/electron-to-chromium": { + "version": "1.5.356", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.356.tgz", + "integrity": "sha512-9NgFd7m5t5MCJ5rUSjJITUXAH9mEGlrlofnMf4YEr+pz6JlP7cWmTAH+JFmbPnaSW8koVTkuW7pacORWAnA5Yw==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", + "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "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/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-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/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "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==", + "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/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/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "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/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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "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/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "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-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/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/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "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/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/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.511.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz", + "integrity": "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "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/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/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==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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-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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "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/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/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/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-is": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "license": "MIT" + }, + "node_modules/react-router": { + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz", + "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-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/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", + "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.130.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.1", + "@rolldown/binding-darwin-arm64": "1.0.1", + "@rolldown/binding-darwin-x64": "1.0.1", + "@rolldown/binding-freebsd-x64": "1.0.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", + "@rolldown/binding-linux-arm64-gnu": "1.0.1", + "@rolldown/binding-linux-arm64-musl": "1.0.1", + "@rolldown/binding-linux-ppc64-gnu": "1.0.1", + "@rolldown/binding-linux-s390x-gnu": "1.0.1", + "@rolldown/binding-linux-x64-gnu": "1.0.1", + "@rolldown/binding-linux-x64-musl": "1.0.1", + "@rolldown/binding-openharmony-arm64": "1.0.1", + "@rolldown/binding-wasm32-wasi": "1.0.1", + "@rolldown/binding-win32-arm64-msvc": "1.0.1", + "@rolldown/binding-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/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/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/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/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/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/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/vite": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", + "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.1", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "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/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/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.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "extraneous": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "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" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", + "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d26f231 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "property-match", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^9.0.1", + "@mui/material": "^9.0.1", + "@tanstack/react-query": "^5.75.2", + "lucide-react": "^0.511.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-router": "^7.15.0", + "zod": "^3.25.17", + "zustand": "^5.0.5" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.3.0", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "tailwindcss": "^4.3.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..a1d85a6 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,55 @@ +import { lazy, Suspense } from 'react' +import { Routes, Route, Navigate } from 'react-router' +import { LoadingPage, AppErrorBoundary } from './components/ui' +import { AppShell } from './components/layout' + +const SupplyDashboard = lazy(() => import('./pages/supply/SupplyDashboard')) +const Properties = lazy(() => import('./pages/supply/Properties')) +const MatchCenter = lazy(() => import('./pages/supply/MatchCenter')) +const FutureAvailability = lazy(() => import('./pages/supply/FutureAvailability')) +const DataQuality = lazy(() => import('./pages/supply/DataQuality')) + +const AISearch = lazy(() => import('./pages/demand/AISearch')) +const Results = lazy(() => import('./pages/demand/Results')) +const Compare = lazy(() => import('./pages/demand/Compare')) +const Shortlists = lazy(() => import('./pages/demand/Shortlists')) + +const ReviewQueue = lazy(() => import('./pages/ops/ReviewQueue')) +const AIMonitoring = lazy(() => import('./pages/ops/AIMonitoring')) +const Governance = lazy(() => import('./pages/ops/Governance')) + +function App() { + return ( + + }> + + }> + } /> + + {/* Supply Workspace */} + } /> + } /> + } /> + } /> + } /> + + {/* Demand Workspace */} + } /> + } /> + } /> + } /> + + {/* Operations Workspace */} + } /> + } /> + } /> + + + } /> + + + + ) +} + +export default App diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 zcmV+cGycqpP)V|)f$;Qooc7=_G zlYe)HToTQIc!$)^+J1M1y0*T%w!p~7%ux`!eRhO?c80XDxKQ*R^lUUMnA>6NT^?feoZ8xxvP32D&s-9ow zqjcM}eesrC)NeDmsf)*P7wJ|K!&xP%Zy4iI8lF)Tv2!reW)tCzg_1=PmOwd1SQfxa z8;58t!=z~Ba7CYlNWVG>he8aRPY|+-JmozNhn!#9i#77Aa_Edt$ijyCWL#=~I>~2X zZNrQ8I0=D+NWD4pq=7~(i zhfThMNw|G>g^y9pGzxX7ZSApl@tIxFcs{p#MX{Ax&XZT+cR#U+OWc@S)pkIuI}dzu zH?^Q=<(y&Vq-oxSLfc0Zmq81bjZWf}RnssBaD6}2g-XJHLcN_|*IOu>m|x$nbm(?E zyNy!Zp=RroS;?Vg*kmoJYBi!n5{_^@rA!)=t#a^;N$8GL!*DsQb}`yvEuX!G@||An znOfUZAevPrkV_qjl|<~3QRZzG&h@C9Y5z zqpNH4xqbF_InIPh)kX}Vn^5kyed|mOuq+2>M;v~KO37a#yrEn3XDqtOl=rc6_KZ!; zreo)DFVB4|>1Zd(bvMI%8uM;3!)YMYu&cG?(PE!B~y@3yKBMt|R zAf=I16tFwPsl)!jDqvYkLHaAQ+f@W1m6F5aZvwhm4JL z{_l)@b;)mDSzle2gyFP5-r1x-5X{G}ot%VyWP@vEW80!Q=f%RTfpg>B*TA^pyWYUQ z<=xPtz}WcZ!;rFl4m1D&FFHv?K~#9!?A%+fn=lXt;9!Fc#kQ;zk~gZFsH z8e5iu@c_pzX&qb8&Dum*oXwB+fm6l6gFfC|o*wgEiy6tw~&co z9Vd_4)P%wP-KwQW7|lN-znGK#?N+j24U=$982myIBM+vsiKsc*@4-rwJxuAaHKna6 zT3wi!C~a4ZKH03qU}_1bKyx0&$CaK7_%Z+Kl$)fF5^op zZApQF2TvDav!s|krTjw-8US6ep z%!VmX4luub+fseQz_D9ATJQ?iQQwD}TZz{-yo#l12a%+7bT@E(X-hyaVS-5vuXc#^ zx^w;L21;NphGVoj*{s3f4dme0y2LC=G1-7THd`#z?;tuC{^9k(dM{Rf2GOxg7Jzho z7nSZHl7?M9kdalX`)YgoKEfiae5+;$(OGeN1eqxrv!ZCVKyH>xiyNqfe8xzY8*7)H zQls8KMp)F4D>ED;idMOU^^WhVF@q>ZSmeB0y~qC~|DB648hr%Sh|*T(4q|w2l?m2+ zvBVw3@7+Mz?^Yc#+se6KM;a<=(W-I>k)$-qL2V*t}VaW`;?P4)WqI%maIDq8!oUcSYAD`}wWjkSyAVsnF65#2zQ zZ>(K*TlS(E#4y$4Zq+e^_&}d)q20hCe3!LfLYP%nQpLJ~gM6a1hJlz3)aS<9C9me| zAcmJ#>tOwBy{HoP0Sm1&_(E+S@6 zgBIFUoei8zJmdpiq8q5=OY7t@`)JWxn_&GvKVr=Zdb_pEL_j|=?f;WK^U9Q0efd#K z9q7SfJTl4pmA$jsZ5oK8@O9#!I3Cv-kL)<8SalSsp#dcpvJ}Nz#G6FC0%9|7Fi#8; zGDJXtj!&GljT3*HE@0EE>G8Se&d)*nkqe}-?`3vPl&UqK?xG z!3XJ4M-x`EuQjhBbu?ik-)rmIt=DF_N?TVMP)8Gjn)TZ2V%H|zENbeix}kOxd@0}Q z>)HuH6Ean!uS#~4g2Ne2WsMGel|h%j9*W_quQheG^JqmKhc*RYzp0wKlGjBq2VzY_ zgOv8WC1+%W=W)k)Yp_`8kfE=uiiwOZTXi8Uj9YGr$f@yJcJ;#&-Nq~sJ7anE(@;QN z=~br%7%7`isKStX|7!1?L(apl^QvPKlrHV4S+6tNVQ*R1iGdC~WMNE1$a+=rpQmcB z>wxiLIBvOnm;u*;9Y!kJdy(T4lk|8>JAm(&wEsFIF1$_*{>2ZNd$V6DS=SfrGxAv0 zzKe377JI`&o9Ljr+VnS*EwehA{f&{cKZF(6*MG5!p5MvrFA3ll{fmRG*L@6^cb;o^ z3Wm8c?Sc6$`>~VEWw(c$Y?nRO;2Q$=ulpqPtM^=1IZx;@xK0PgO7rKQ^WHVLwtgUT z%|JF{^f(VH)wLKQ%dYiu2RmchBdxL0-M?wxxul_z*{h6ZZ`>-k(vizs((vW8Lt6Z6 zY;Dt?@JWyN`O`f;&d1Mb?e%9oyRK1ql?EE5XB2(W)|D1~Rx35$H6@6)$F?)7V|zEO zI}fu0-0}8W5=6sg$fPnZ~7=tTudl?Ecb@pxbo)vni%gP-?hL|%*?62C;x6?@E`VRnJv z?fTb;k4x;TS7Cu-z%J}uy}e-pwpLQ17Q@4DC+FCdAmNKklG$`I_pyw7E{fYmw~{Fj zi?6KcVy=Wrel)EB_DWO|0CKmI|13!gBV?X`Ozp7x>?6jr`>Qz=^4ea35!$*f}) zS$i+x_k+@P2q1RFUH^ZTTk7=n?cjfR>hTq3l3SY~#w+I8SSutXGyhw;Ws~=zMQ%Vc z>$On~47Ut?P*_!TOQ&PFmLAyJieB2X4_Fd_!WxI-AY`q1Lc-oK?+qcOTzlQ?@~x@OT}*9jTVNfl@3rGvZpWI=eKg>T zZb@6YWz)J=IhP7CF|c?G62vMEG%#U}?#86$0jR4sG~i(jRd#jmn`7b(O#?N;3a;1t zhXLssmUwGhp79luw#(*V8WL0|8+E z6=YZ_O@er~$LrD_PYGc(kJgB=;yw#+Z3X6LDUZ(NcwN=B-hjdiHm!JFar%m{(5bEW z@@_VEtG$5;`EJZ|OkJ@l&G9n((w@uNFwmU%bG|s#TbcJJos!{e+bjCjrCq_}LcN!UFgKtgg7siV*7# z!}1whTRRi*-avJPu->C}Z8EiuK$#886+H_#_!btv+rsiBbv2jAJvJ+O0{#}y(%L3H zfjU-kq_-L@2XrL*ae{{qYJkD{@dw%*bkh2P&YS-0!Xt!PRz7KHV0+~j(t9W8lAVWR zt@B*DgURgEz4>WuN>o?_iKcw$?k{||Pg7{Q2o4|VmJ)mg?{VQJA<}zEr^YAAS zgGm5RT4T3p)U;yz-tfBO^kw8?IoG!IVmc+Z3m#}AOQ?5MRa>)OcU!$N^_+yK6ayn? zK>~WK0!#ysuj^oNLakm)Zvu+J)OSubX^kv!c*xgdIvs;kln!rgG4*uZ;w0mQQO4XD zO9P{GNdv!=cQ(CAL{S(%KtuV^zC&Q{%g)PoXnp^gn^>c*`E>$hLYg2HjnbVGtWLa{7zHdG1jT@B{|Dm16 z7K2(jsfG+m*Zxof)iXxu+!H5Mo-0$pkyV3VV4B@Qms46M zuBxGRV@HxU7Wwx-6CB zaU*HO<_qn$5GH>&@?nRy1{z zkik!sLfWQ)r#75)vVwCBU*r_)Q6mp?!j85{#Xqse)ApRdE$V0%I0*~e(_{)5H)`Mk z#rExC>yjhZxuL@|+#v4#<Axw$+VpV zuT;!2Vww$je$DpAW`$FX_Ab|Ip%$;&T$-lW8jS~B$>G}rd>eQG+$h9lQx4Mx0w={m zx9?T6VU`>sR}XClkAhHEShOUe8awiq zmizhL+}5UKs3}6~It7vBTig9dfQ2Q8coo+Miiaw7n~>4ybv2Ptt0^^=VqX(t*Yya9 zr`FxxFX8(v*H=+uJ#JJWIB2A(==HDYx~^zZ2nu?2`}|Wsa*f3h3ixc+U|FDtAG$Y! z*lc_7se5Oso-Cgqe0){{!8H4g$3<8!R<6JOurD;((({c$1(pwb>(#TT!sge@4>r2@ zVL7>U`0`nsWAYErezk4(Z!gMI2?UTo{J3Ajo(u4)KYIRd>BRcG4BoS3G0EXyEp@tw z%P7__?A^a>Q&AKL@ayDO9D*Qkc!NHnO9l}kpp_6hXbMppYL(X1L?njdFT|-h2<_$; zAtDZ!1Rf%|yb!qbWKd}%0b`LzBeyNy43|QO(&h2mxQLUL)|0%agVOW)6TV!&Ip^Ls z`PG2cygM8)IecQx=Fc+nqYRo4hS^^-nM_&-y8?EJXUczP=DIw(GkTJdpEdh<_STs{ z|A)4n1GKdE=Wu!!nYoZHcUQ4S&R;oDOKX2lrkdF(mK>hz<$Pp>igjOcvoRIjlN=W8 zu8Gx5(roqn8$>gEE5vy{GiGeW8Tq{vnf3hS-V=$tZkQuftUVuU8o6k&dn=Yg3)6MOIH>nlK^-2+C6BZITr~1@So?NvG#TwL)|~=1YXGMTLpS<)ziK_CSOabe z=cB#5)yz|@0i9dSo?*CX)}UP=s6)B+F@~Em(u@Q(I9J9i_V{LmMu8BfXYMh~*oPP+ z!3~xTv|(>|=n6ZOtT~C@V!z!w%18*8T2t6}U2S##rC)mekBql&VsBX;$~ByGE$oA9 z`0Wzq8p?R{4)$l*on;!cLa}Dh^Xe?owiQZt9nH1fxxh$pN9K%CtOw?u3>85L7rr!d zXs)l{TZ{xXP&U8exz?9cv~dNNibOmt*K4I$?RxqIBZ0(?Mg-9FS{*9Bc49Qc1`=sIF-rye`aNT1G@4NwXcnyc@+bw_mTsR>5< zF<2;X0QesG_pw|TonqVBhRtfqI>ty(SIu&VOXd0CrLlfp+;WH7HYjhqnu^oAY!9cB z=B6#R?Rfz9BP`dJ=@v_?70s3HxQPk+{6Y+lM85f2NF^00*^OcM0~?JOZfR9ZPYF+# zYSs}(_BUYV8{n@2a1hD^SV41bwmi2uztR;PeBgF1F-`9>`zoNss-@3LaF2sjl~>OaaVmp7PNp+UT`6@}gR%uzqHDVeEZ14{Yt?n%JeQm+t(1_u zSc}oj^{b;+rlS|ME%+LjzSI&xu0Bblxo$MJ-J$kJ?Qu_XUXh}*@*-x@ny|}wVM%Lg z3tNB`yvr*}N?ClGL;H2cglcvErIccU3(eP7>@~4nOIcI~-`P8tSQnx=jI&{9)!1}l z;gQ%_h>ZlPSV@o@Azq1R$C6ja5!^ZGh;YRhhxs58qJWo9@Bceac&yy(pET1hnn`~7@}2L0&dfPKYs$ih7m2}R!25!(hxqA(!UIw; zK4+~Jowy3=RNC6nE=ncU{LH5?*9@W24lacJlvCZXB$CYtE@>c+~H zkV=(5I&gb{xn2!~f&fs2NQgAL6`p|kyt6kpWk}iVlqIp(H;ig`{_U9yxs1jzu^ETM z7~)Rg8C-NueqTYP&U8l{DY=Y47cR zOR@U%$KQV{mkRF|4)z9Y^t3K`@p>duY&QLUFeh6VoV`a`$U@)(z!-N*5Cj<11$EZW&hJLX83TO{lJYP74rlDZQPkm@t<=U^I)x@|UnHHkdQlh?!ltZwl92rE;;^ zZuIappj4dhld1}kttYYV-j|KF1Kus zWBnzttD^00%LFK(wrwNragFub6xiV8QE2rm<`&fcR4SLFcdtLxVuN!Aal-g6dE4%k zARZ}|xeo;K{0yf7@9aua%2j5o)CPcIOc6uLHFJOcgtB5owlcNAwyAHc0QB0Dts?c@ zUemG~j_E&W7R%+x-IO4FJl8e&*2Blmp1S#RA|)geVrxvP)NHdYuxi~g&Etn?QdNK8ZDKZ?QFLU?zh30G|t9G>a_X4zk}Ygw<^$7K!GIn(Io$>(d4ODJQ2XSd%jpK zm7>ptl$a3GyB}5-%p4>Q*p#VL^B{yQMuFCM^#l#+N!Ne z5_PrJWB=@Iy+t)H`g1lX`{bm($KE5I?0c(JEYm#t{F}j!xtsbob0{xu@0TB_*>G7w0ICn zr#VoBktqHZ~XxhiKD*lcG|b;H*|Ny3P^8ceV`sfBRfrhwZ!T+MFZ!F1Bt{q$8d9i6o?~ zODj^POr}&ivSa^R^YFIq7o0giLBKCycH_aU`F6)O6JX%nPTwh~Q`eq6*0iE#Srj2^ z*_hN3%*b83zfafy60@Cp3{J({RlSaEn&E?mrxRNC9GQ7#+f=s! z0KBf-9Ny_v2VbE%aB|Di)5kNJ^t&C`4D(>t7zYUWUFtbxt+Oq=!@O7BU)}>d*R72o zFF)3jQD_lLe4is&xzyJYC1-c{8TX$RU>&>P$%)ufpez0XSAukmh!xcekg`s$c<>-q zI#zn^JU0zzF}V60)o$_gY}PQH>b2M9&8fRZa#OauglPb zeQ@pMm&=!vNgos4CluQjLMV!pfkmxK+35bi^k&=k>9h02?l+u+m0agG;(h2|Jslc-llvtEwn~*w3bx7qnvZACG<8}AGeaDVvcHbKd2>3G^ zSFPULUn-?Pmo^-_`mLZr??uNH`2=I&yajlrF{DtUxMy#Nu}z=3y7qbUA;5`)hibMR zhXL@@uKyV0-2&A@t@!xyrBnMJl&^o@Gx$&5_q6?D=ji5grd-~=?dlg;ur(_V0wjh! zA=JV^C1m+DDkOsgr<%O9ZQFg!0}pD(#PSz4Dr_EyS5$`)VIAv);4n-SFP~YtC7sH= z7&*MfpH;gd*FHbkmD#)hVxb6xjc9~`t?_{=JS+@ip_cTicXxG<=7m9& zPX+Z8IC*GSAXuGCrZDHgR$r%jyk-fctis2Kx4HvZ|B~8uC@o)m^>Hy-O!&TKA?$&n zkP2Xc54w~!=z2?^NafyL*L0V9cbYrugHBBUj`xVyZmGFR&kvk#>1J*Z~i zNTz}?IAdJ$gkqd2!Gw(%LzE!O5s4C7q4%T~e_P{+z=DNDKrG**p=U`d5yg^vp`;Zn zsU=8gd0a9s4s0FPJePWR9eH5=+O^Kks&kC-iblNqTh2&Pw*^(4384f+D8N|fewZu_ zg2ejQ)ov;ztz;NQl7yj;A`(!H!XQu_$sqY9h_IrH*}_%1{L&_YLDvO?%R5Z-t+ClW z_qERbL?HKUZ!nt+!E9S`uoh^5A|DaIHe*_gf1`E_Vq+}{&T@t$EGhMnRjJ4z2w_W8 zp+qjs7as22^&S3wY1?+}^j-I=RcCE>#|39)g(lU7v_8;?=qK(9D8-*pPdiy)P3lIblG`+?%ea| zYoD3dopYt!tKgFicfNmNi(EWE=E4hC6(r|PYtanqJlmt57YOVrr2^tfrG(eG9C##X zu&1t@%L$RIvpj!wUA z8i>Pqot#_+Cnp6L2XPcZy1ar|9MnY+7eNvK1E)@Tr#2KsXq1*>)uUCozT7L##ok?o zhA6ofP4E|b*9tAfG?uf$#}>TIR&1A!yslP8}i7w-EzW(x#9VEvx18k%Tn=-$VV zkOtUr0b2!w3t>h?#8AZl^Az*(6KCGlD;4j~yx};`#2gN1_gv=%7KVzecIRakN{f*4 zeaI>yH;-o4OGhvGTU)(quWI)-q?V*(sVesSMv|wMUQ3hLEt=lBB$KZ9TyHr>)f7o%) zPYeU<3P)*P10*7vE)nA5#{c=6-E-_>r_u4e3i!I2+UksELwDqwMeBZ9FSP$;^Ajro z_@M#_Ss$?ejoB@!wN|kbGKs(0zLo%0QpQXW#t;oC$B0MZYZ&Ej?8~fNhcCVvPo3vo zFn0WWZaPliF^8_}yzb`*f@yg0uWv6HgNI)xa=pO%Ck(C<=-60l#uD3(wXP~c7!NoX z0&^6=N`zcc90F#qt@=Rn@r!3(*1v(Tl{B!m?Mc7yIA+nEHpY{YWr$=)F7rhR1P}(v zt{YhY#;jsW6G>#xhP*B`OCk|Pf+NN;ju1rxa*HAgoGq*rvqw&xe~;t1JA31$s?GBb z*g7&@cbKo4n<`>)!UlIAgR6q&))B0KYU8r66GbFj?8Guw4E%&}Qi_lT003LtoIZei zwD~=XZmeo+yZ2Pq3KYCF-R&11^p= z@H%s+=G`}wrbJ{()Mh71#2SP3Zy3m>l1n?0N-N1Q;z6?oSxr-G(H5m4EO>~&;}VKi zfY}3w+9z>vp#d)hVuu`)vG_aaH%3b=WKMnSu&c31;<3O;bz2iD=w+o4#oBb36 z5ZCF*Gu?zjZIR0S>_%pHY2$k8D^n7Sz_K8tCDeXM+dO<#LSg%h6`~dnVG1N@T7v&e z%wEd1!k{^zfz_1BTW{!$!B%g)J^2b87!9Y>>100X1SgT7s0z$o>^lAA=Gp_cC1(h=*5Tmf8z&LGJJ>$|K^~s`z9*OWz5MFUr?>Bi?_PGBB)#psD5?>n+q{o_ zz7~ez&;t#h8l$jwGPCC&xq2YetXYQT+0F3j(`xmNGf8dj#an|p#I*pvI*kwW4iuB> z+q3_7xB8y;pLzHG-S%+UHQA zvqp;$kmGJY>lLsN4C~&TcvAS1SErTcwcw0r@wngk zShAUA1M9b#g}^pL-zH7Q#z^&j#r9F8BTVfkR&qF<=e35goTu7c|GN)0mokj4m0%~0 zXJ8j4Hc_l;HJ&uU*Iw`8d_EscJ``s0tk9mkKo^&#TYXm-EoAzTQObxa@^u~g2t#T) zJz|rE!I_?i4dCJC=B8(_pZ{YR>|V?0iCcnU;E@$239^x?SYCfNaMHN;CtHIS_zHN9 zTkQc1v@O35okiFtq5_u+5FkY55ap@pi)O?}x0D1c*qB0KpYR}>Ul+B0Vmr}Z@+%mJ|As}sis_=ROPbov@*2thpE&?!V#Qgu$snYvCZ zrkhmkMU+fSf-s8(L37fPr&M*jRs{{THb!aXQu|P9l_-vJhHvLzMGH zE?1U0H_+PmNABp9`|KzkGfrrZ%XvdGo6*<{d5m9~L7 z_^`M;X6xDo=m6LY6RfvJEvsTK1!u8d2HPx|$S}p;sRy!I zWL55Yxu~_B`OP@~(q6&W3#)~I&+MGL%GWR$#udC151^wsswhqlii;rP9jJpiI7o&Z zAb})=HY7?4HA|re3ns`%$)FuvKCFWjhb~?IE)F6dF2K5}poj-NK6Gf;hw$t3=1txY zoxQxZWrQU6K!%|~!m?~Bnw-6Rr!F3BZ{u5!LqnZTDON}Coj9^@&le)V!NYrVwS~B% zEL+>Sr@}qGwGvu|HrOo|gSt__ezN^&%~{*)a=rf7y1HujUcr`zZB<4#l@T#eN)si} z)lZA<{=tKx8E%c9>A(##6}_p+~EZpKsl5a4pj`E*;_-6`ysiv zffA!7=MT1vCz}-m4~tjVey1b2KSR4OEtLd-(_DdUqYZ74LaDkhH?KFh?%WAOP2WbX zp@zT+Dx|5_f%JQiAGvVw!oh+g3e50u!aPfMxdC=E)XB{F5IcEZhePIM- zph6Y`$Oy?JBL<8Ex(SqEhLeQ@XcrdA>a?rx+_~HLA;l14)WmmpH}_w?Pg#HBZs0eS zwypwAW?M-x+3AU-(GGWSJ=ngxUEcEZ5OsX(Qlt!MQ zn^(`S{GHkAv(8@D`EAfSYig%Cxv?z!{=w^F#y)5_d7FuKZH7qlR-#5B0bt806%D0I zT7VdVP_?q*%Rq8UR;JkD4i^RXowt+E%#V2U>TfDqzZSDZ+dR!a#T3I>-z_$q9@k|m zy5~A*m~&JWP@E7a=pc}4kVHTc4h&R;Li7d@f`|hKMLkbb^uhOakNr3&FLjlm~i5NBM< zFaYI{;cpiHCNRdE0dg*>qIm(_t?#$h=(SCw?h3rJV2*ER8{O4^3#=dO)KwklZkoqU zS8i5c%YL*y*4;FY#D=XmkQnYj%LH)?02~gSJH`Qp1XY64g>%c_K$xseI&|e)7vRoL zAqRba$G@%fSGA7X7hQk%_3NVOYVS+$leU_!&6*5uN)8#5ZBz_6ASCA;azYS-Rt@ki zg2NWz(=;t}SC(~Ibl63$5C8FPmhXqb^)5#jaJ~I{Ex3xZ!+2h8$}}h_g@Be>HZ;72 z6#y#>AY3^skuVKF#0WxFBQ()5d5_nWb?c6c>EeMM|Mh+*&wEpPyxHCq{R-Gdr-`hN zF=1sxl&mBoK+#qRLl9#CEN|Fg8>nbmsTg3a1;#M9enQ$RgWk}kp#-5wh=EF&1tl%mJln2V^8o%Qv(*=zEuO7y z=m*8?xpUn-*@h5Cl_3BK3joiGkyaScK+>|MWdMRWm@RT!Q1piAlv5hL@B6>3&GI8) zP!xBc6}ZNIpJLL%2a8Y!+(<=f%WX>_uWVxlga9!D*oYt$l0cxRDMvqfU;Kq_mLK5k z)dvqYcgLa_Lz?3HyeF)@$%$&6lI?r4I>6W#M*<)vq{?&Oqrx``d`mhpVPr> z#q078F6gw_X<=?KR>8%^t%@wbITvNMu!hKiTSkCTJkw>1!e*Y{%31#_yMf=LW7{RJ zYoC^w$6%3cBtVG5)x#{Hg6IVTh9XEcM{gQwXk!R^y95^f-hZ`d{aVa+xW1EO4wDV4 zB?JgD7*?qkvc|$nIykTvNl2x0j3Q!MXoLL^)~}d7jcYf(H8D~c+?$pKL(px>Z3`eb z04RzS6_AgFT6Pn#iZAg$Sl_j8#;6ShF%&(Fag#E2asU@@LaN;=b=Wf7sgPKhfzhBM zC@eFL8^MrnA*9&Khe*Ab@CC9*uyJGXyi(;y2>lQLJZt;ShtJi?3Yf_t`F+$hY!+Q2Ndsx=U+bjTiAy7djLji>7k%k`$9&--f<*BNA3Hy&ZrHH|4 zG5H&9cB?O#zI1_OOf0Ce%mDfQxdtp3vU%(iY6yji3iISS61XLv#z|!zI_sZqza@B+ zyu9st5-h+`H7QUKx9}3w@oU@EO}&cEzG?fu!!bLO->%zkcg;i9^j`S~=WKMnDi1f= P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx new file mode 100644 index 0000000..4a7a722 --- /dev/null +++ b/src/components/layout/AppShell.tsx @@ -0,0 +1,527 @@ +import { useEffect } from 'react' +import { Outlet, NavLink, useNavigate, useLocation } from 'react-router' +import { useLayoutStore } from '../../stores/layoutStore' +import { useSessionStore } from '../../stores/sessionStore' +import { WorkspaceType } from '../../domain/enums' +import { + Box, + Typography, + Avatar, + IconButton, + Tooltip, + Chip, + Button, +} from '@mui/material' +import { + LayoutDashboard, + Building2, + Target, + TrendingUp, + CheckSquare, + Search, + List, + Columns2, + Bookmark, + ClipboardList, + Activity, + Shield, + ChevronLeft, + ChevronRight, + Sparkles, + Bell, +} from 'lucide-react' +import type { LucideIcon } from 'lucide-react' + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface NavItem { + path: string + label: string + icon: LucideIcon +} + +interface WorkspaceConfig { + label: string + abbreviation: string + icon: LucideIcon + firstPath: string + navItems: NavItem[] + chipColor: string +} + +// --------------------------------------------------------------------------- +// Workspace configuration +// --------------------------------------------------------------------------- + +const WORKSPACE_CONFIG: Record = { + [WorkspaceType.SUPPLY]: { + label: 'Supply', + abbreviation: 'SUP', + icon: Building2, + firstPath: '/supply/dashboard', + chipColor: '#1e3a5f', + navItems: [ + { path: '/supply/dashboard', label: 'Dashboard', icon: LayoutDashboard }, + { path: '/supply/properties', label: 'Objekte', icon: Building2 }, + { path: '/supply/match-center', label: 'Match Center', icon: Target }, + { path: '/supply/future-availability', label: 'Marktchancen', icon: TrendingUp }, + { path: '/supply/data-quality', label: 'Datenqualität', icon: CheckSquare }, + ], + }, + [WorkspaceType.DEMAND]: { + label: 'Demand', + abbreviation: 'DEM', + icon: Search, + firstPath: '/demand/ai-search', + chipColor: '#1a7a4a', + navItems: [ + { path: '/demand/ai-search', label: 'AI Suche', icon: Search }, + { path: '/demand/results', label: 'Ergebnisse', icon: List }, + { path: '/demand/compare', label: 'Vergleich', icon: Columns2 }, + { path: '/demand/shortlists', label: 'Shortlists', icon: Bookmark }, + ], + }, + [WorkspaceType.OPERATIONS]: { + label: 'Operations', + abbreviation: 'OPS', + icon: Shield, + firstPath: '/ops/review-queue', + chipColor: '#7c3aed', + navItems: [ + { path: '/ops/review-queue', label: 'Review Queue', icon: ClipboardList }, + { path: '/ops/ai-monitoring', label: 'AI Monitoring', icon: Activity }, + { path: '/ops/governance', label: 'Governance', icon: Shield }, + ], + }, +} + +// Ordered list for rendering workspace tabs +const WORKSPACE_ORDER: WorkspaceType[] = [ + WorkspaceType.SUPPLY, + WorkspaceType.DEMAND, + WorkspaceType.OPERATIONS, +] + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function getWorkspaceFromPath(pathname: string): WorkspaceType | null { + if (pathname.startsWith('/supply')) return WorkspaceType.SUPPLY + if (pathname.startsWith('/demand')) return WorkspaceType.DEMAND + if (pathname.startsWith('/ops')) return WorkspaceType.OPERATIONS + return null +} + +function getPageNameFromPath(pathname: string): string { + for (const ws of Object.values(WORKSPACE_CONFIG)) { + for (const item of ws.navItems) { + if (item.path === pathname) return item.label + } + } + // Fallback: last segment, capitalised + const segment = pathname.split('/').filter(Boolean).pop() ?? '' + return segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ') +} + +function getUserInitials(name: string): string { + return name + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + .slice(0, 2) +} + +// --------------------------------------------------------------------------- +// Sub-components +// --------------------------------------------------------------------------- + +const SIDEBAR_BG = '#0f1923' +const DIVIDER_COLOR = 'rgba(255,255,255,0.08)' +const TEXT_MUTED = '#94a3b8' +const TEXT_WHITE = '#ffffff' +const ACTIVE_BG = 'rgba(255,255,255,0.1)' +const NAV_ACTIVE_BG = 'rgba(255,255,255,0.12)' +const NAV_HOVER_BG = 'rgba(255,255,255,0.06)' + +interface SidebarProps { + collapsed: boolean + activeWorkspace: WorkspaceType + onWorkspaceClick: (workspace: WorkspaceType) => void + onToggle: () => void + userName: string + orgName: string +} + +function Sidebar({ + collapsed, + activeWorkspace, + onWorkspaceClick, + onToggle, + userName, + orgName, +}: SidebarProps) { + const config = WORKSPACE_CONFIG[activeWorkspace] + const width = collapsed ? 60 : 240 + + return ( + + {/* Logo area */} + + {collapsed ? ( + + PM + + ) : ( + + + Property + + + Match + + + )} + + + {/* Workspace tabs */} + + {WORKSPACE_ORDER.map((ws) => { + const wsConfig = WORKSPACE_CONFIG[ws] + const Icon = wsConfig.icon + const isActive = ws === activeWorkspace + + const tabContent = ( + onWorkspaceClick(ws)} + sx={{ + display: 'flex', + alignItems: 'center', + gap: 1.25, + px: collapsed ? 0 : 1.5, + py: 0.75, + borderRadius: 1, + cursor: 'pointer', + justifyContent: collapsed ? 'center' : 'flex-start', + backgroundColor: isActive ? ACTIVE_BG : 'transparent', + '&:hover': { + backgroundColor: isActive ? ACTIVE_BG : NAV_HOVER_BG, + }, + transition: 'background-color 0.15s ease', + }} + > + + {!collapsed && ( + + {wsConfig.label} + + )} + + ) + + return collapsed ? ( + + {tabContent} + + ) : ( + {tabContent} + ) + })} + + + {/* Nav items */} + + {config.navItems.map((item) => { + const Icon = item.icon + + const navContent = ( + + {({ isActive }) => ( + + + {!collapsed && ( + + {item.label} + + )} + + )} + + ) + + return collapsed ? ( + + {navContent} + + ) : ( + {navContent} + ) + })} + + + {/* Bottom section */} + + {!collapsed && ( + + + {getUserInitials(userName)} + + + + {userName} + + + {orgName} + + + + )} + + + {collapsed ? : } + + + + ) +} + +interface TopBarProps { + activeWorkspace: WorkspaceType + pathname: string +} + +function TopBar({ activeWorkspace, pathname }: TopBarProps) { + const config = WORKSPACE_CONFIG[activeWorkspace] + const pageName = getPageNameFromPath(pathname) + + return ( + + {/* Left side */} + + + + {pageName} + + + + {/* Right side */} + + + + + + + AU + + + + ) +} + +// --------------------------------------------------------------------------- +// AppShell +// --------------------------------------------------------------------------- + +export function AppShell() { + const { activeWorkspace, sidebarCollapsed, setActiveWorkspace, toggleSidebar } = + useLayoutStore() + const { currentUser } = useSessionStore() + const navigate = useNavigate() + const location = useLocation() + + // Sync active workspace with URL + useEffect(() => { + const detected = getWorkspaceFromPath(location.pathname) + if (detected && detected !== activeWorkspace) { + setActiveWorkspace(detected) + } + }, [location.pathname, activeWorkspace, setActiveWorkspace]) + + const handleWorkspaceClick = (workspace: WorkspaceType) => { + setActiveWorkspace(workspace) + navigate(WORKSPACE_CONFIG[workspace].firstPath) + } + + const userName = currentUser?.name ?? 'User' + const orgName = currentUser?.organizationName ?? '' + + return ( + + + + + + + + + + + + ) +} diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts new file mode 100644 index 0000000..391bbd0 --- /dev/null +++ b/src/components/layout/index.ts @@ -0,0 +1 @@ +export { AppShell } from './AppShell' diff --git a/src/components/ui/AppErrorBoundary.tsx b/src/components/ui/AppErrorBoundary.tsx new file mode 100644 index 0000000..3f67fdd --- /dev/null +++ b/src/components/ui/AppErrorBoundary.tsx @@ -0,0 +1,45 @@ +import { Component, type ReactNode } from 'react' +import { Box, Button, Typography } from '@mui/material' +import { AlertTriangle } from 'lucide-react' + +interface Props { + children: ReactNode + fallback?: ReactNode +} + +interface State { + hasError: boolean + error?: Error +} + +export class AppErrorBoundary extends Component { + constructor(props: Props) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error } + } + + handleReset = () => { + this.setState({ hasError: false, error: undefined }) + } + + render() { + if (this.state.hasError) { + if (this.props.fallback) return this.props.fallback + return ( + + + Unerwarteter Fehler + + {this.state.error?.message ?? 'Ein unbekannter Fehler ist aufgetreten.'} + + + + ) + } + return this.props.children + } +} diff --git a/src/components/ui/EmptyState.tsx b/src/components/ui/EmptyState.tsx new file mode 100644 index 0000000..8a388df --- /dev/null +++ b/src/components/ui/EmptyState.tsx @@ -0,0 +1,29 @@ +import { Box, Button, Typography } from '@mui/material' +import type { ReactNode } from 'react' + +interface EmptyStateProps { + icon?: ReactNode + title: string + description?: string + action?: { + label: string + onClick: () => void + } +} + +export function EmptyState({ icon, title, description, action }: EmptyStateProps) { + return ( + + {icon && {icon}} + {title} + {description && ( + {description} + )} + {action && ( + + )} + + ) +} diff --git a/src/components/ui/ErrorState.tsx b/src/components/ui/ErrorState.tsx new file mode 100644 index 0000000..2b0b629 --- /dev/null +++ b/src/components/ui/ErrorState.tsx @@ -0,0 +1,19 @@ +import { Box, Button, Typography } from '@mui/material' +import { AlertCircle } from 'lucide-react' + +interface ErrorStateProps { + message?: string + onRetry?: () => void +} + +export function ErrorState({ message = 'Daten konnten nicht geladen werden.', onRetry }: ErrorStateProps) { + return ( + + + {message} + {onRetry && ( + + )} + + ) +} diff --git a/src/components/ui/LoadingPage.tsx b/src/components/ui/LoadingPage.tsx new file mode 100644 index 0000000..c42ef6d --- /dev/null +++ b/src/components/ui/LoadingPage.tsx @@ -0,0 +1,21 @@ +import { Box, Skeleton } from '@mui/material' + +interface LoadingPageProps { + rows?: number +} + +export function LoadingPage({ rows = 5 }: LoadingPageProps) { + return ( + + + + {[1, 2, 3, 4].map(i => ( + + ))} + + {Array.from({ length: rows }).map((_, i) => ( + + ))} + + ) +} diff --git a/src/components/ui/PageContainer.tsx b/src/components/ui/PageContainer.tsx new file mode 100644 index 0000000..cd77c2e --- /dev/null +++ b/src/components/ui/PageContainer.tsx @@ -0,0 +1,19 @@ +import { Box } from '@mui/material' +import type { ReactNode } from 'react' + +interface PageContainerProps { + children: ReactNode + maxWidth?: string | number + className?: string +} + +export function PageContainer({ children, maxWidth = 1440, className = '' }: PageContainerProps) { + return ( + + {children} + + ) +} diff --git a/src/components/ui/SectionContainer.tsx b/src/components/ui/SectionContainer.tsx new file mode 100644 index 0000000..f77302e --- /dev/null +++ b/src/components/ui/SectionContainer.tsx @@ -0,0 +1,29 @@ +import { Box, Divider, Typography } from '@mui/material' +import type { ReactNode } from 'react' + +interface SectionContainerProps { + title?: string + subtitle?: string + action?: ReactNode + children: ReactNode + className?: string + divider?: boolean +} + +export function SectionContainer({ title, subtitle, action, children, className = '', divider = false }: SectionContainerProps) { + return ( + + {(title || action) && ( + + + {title && {title}} + {subtitle && {subtitle}} + + {action} + + )} + {divider && } + {children} + + ) +} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts new file mode 100644 index 0000000..cdcc7d2 --- /dev/null +++ b/src/components/ui/index.ts @@ -0,0 +1,6 @@ +export { AppErrorBoundary } from './AppErrorBoundary' +export { LoadingPage } from './LoadingPage' +export { EmptyState } from './EmptyState' +export { ErrorState } from './ErrorState' +export { PageContainer } from './PageContainer' +export { SectionContainer } from './SectionContainer' diff --git a/src/domain/enums.ts b/src/domain/enums.ts new file mode 100644 index 0000000..8e62933 --- /dev/null +++ b/src/domain/enums.ts @@ -0,0 +1,65 @@ +export enum AssetType { + OFFICE = 'OFFICE', + RETAIL = 'RETAIL', + GASTRO = 'GASTRO', + LOGISTICS = 'LOGISTICS', + PRODUCTION = 'PRODUCTION', + MIXED = 'MIXED', +} + +export enum ResultType { + VERIFIED_PORTFOLIO = 'VERIFIED_PORTFOLIO', + EXTERNAL_MARKET = 'EXTERNAL_MARKET', + FUTURE_AVAILABILITY = 'FUTURE_AVAILABILITY', +} + +export enum MatchStrength { + STRONG = 'STRONG', + MODERATE = 'MODERATE', + WEAK = 'WEAK', +} + +export enum RiskLevel { + LOW = 'LOW', + MEDIUM = 'MEDIUM', + HIGH = 'HIGH', + CRITICAL = 'CRITICAL', +} + +export enum AvailabilityStatus { + AVAILABLE_NOW = 'AVAILABLE_NOW', + AVAILABLE_SOON = 'AVAILABLE_SOON', + FUTURE_SIGNAL = 'FUTURE_SIGNAL', + OCCUPIED = 'OCCUPIED', + UNKNOWN = 'UNKNOWN', +} + +export enum DataFreshness { + FRESH = 'FRESH', + STALE = 'STALE', + OUTDATED = 'OUTDATED', +} + +export enum UserRole { + SUPER_ADMIN = 'SUPER_ADMIN', + ORGANIZATION_ADMIN = 'ORGANIZATION_ADMIN', + PROPERTY_MANAGER = 'PROPERTY_MANAGER', + REVIEWER = 'REVIEWER', + OWNER_VIEWER = 'OWNER_VIEWER', + DEMAND_USER = 'DEMAND_USER', +} + +export enum SignalType { + EXPANSION = 'EXPANSION', + POSSIBLE_MOVE_OUT = 'POSSIBLE_MOVE_OUT', + CONSTRUCTION_PROJECT = 'CONSTRUCTION_PROJECT', + RESTRUCTURING = 'RESTRUCTURING', + PROJECT_DEVELOPMENT = 'PROJECT_DEVELOPMENT', + SPACE_CONSOLIDATION = 'SPACE_CONSOLIDATION', +} + +export enum WorkspaceType { + SUPPLY = 'SUPPLY', + DEMAND = 'DEMAND', + OPERATIONS = 'OPERATIONS', +} diff --git a/src/domain/futureSignal.ts b/src/domain/futureSignal.ts new file mode 100644 index 0000000..3ff1683 --- /dev/null +++ b/src/domain/futureSignal.ts @@ -0,0 +1,33 @@ +import type { SignalType, RiskLevel } from './enums' + +export interface SignalSource { + type: 'PRESS' | 'CONSTRUCTION_PERMIT' | 'JOB_POSTING' | 'COMPANY_REPORT' | 'MARKET_DATA' | 'MANUAL' + url?: string + publishedAt?: string + credibility: 'LOW' | 'MEDIUM' | 'HIGH' +} + +export interface FutureSignal { + id: string + signalType: SignalType + companyName?: string + propertyId?: string + locationHint: string + areaSqmEstimate?: number + probability: number + confidenceScore: number + timeHorizonMonths: number + source: SignalSource + sensitivityLevel: 'PUBLIC' | 'INTERNAL' | 'CONFIDENTIAL' + disclaimer: string + riskLevel: RiskLevel + marketIndicator?: string + relevanceScore?: number + isVerified: boolean + verifiedBy?: string + verifiedAt?: string + expiresAt?: string + organizationId?: string + createdAt: string + updatedAt: string +} diff --git a/src/domain/index.ts b/src/domain/index.ts new file mode 100644 index 0000000..bd82192 --- /dev/null +++ b/src/domain/index.ts @@ -0,0 +1,5 @@ +export * from './enums' +export * from './property' +export * from './need' +export * from './match' +export * from './futureSignal' diff --git a/src/domain/match.ts b/src/domain/match.ts new file mode 100644 index 0000000..70a81c0 --- /dev/null +++ b/src/domain/match.ts @@ -0,0 +1,53 @@ +import type { MatchStrength, RiskLevel } from './enums' + +export interface ScoreBreakdown { + hardMatchScore: number + softFactorScore: number + confidenceModifier: number + dataQualityModifier: number + totalScore: number +} + +export interface ScoreFactor { + criterion: string + weight: number + score: number + contribution: number + explanation: string +} + +export interface Tradeoff { + criterion: string + concern: string + severity: 'LOW' | 'MEDIUM' | 'HIGH' + mitigation?: string +} + +export interface AlternativeStrategy { + title: string + description: string + expectedScore?: number +} + +export interface Match { + id: string + propertyId: string + needId: string + matchScore: number + matchStrength: MatchStrength + scoreBreakdown: ScoreBreakdown + positiveFactors: ScoreFactor[] + negativeFactors: ScoreFactor[] + tradeoffs: Tradeoff[] + explainabilitySummary: string + confidenceLevel: number + riskLevel: RiskLevel + uncertaintyIndicators: string[] + alternativeStrategies?: AlternativeStrategy[] + reviewedBy?: string + reviewedAt?: string + isApproved?: boolean + organizationId?: string + createdAt: string + updatedAt: string +} diff --git a/src/domain/need.ts b/src/domain/need.ts new file mode 100644 index 0000000..3a1b0b8 --- /dev/null +++ b/src/domain/need.ts @@ -0,0 +1,65 @@ +import type { AssetType } from './enums' + +export interface AreaRange { + min: number + max: number +} + +export interface BudgetRange { + minPerSqm?: number + maxPerSqm: number + maxMonthlyTotal?: number + currency: string +} + +export interface Timing { + earliestMoveIn: string + latestMoveIn: string + contractDurationMonths?: number + flexibleTiming: boolean +} + +export interface WeightingProfile { + area: number + location: number + budget: number + timing: number + prestige: number + accessibility: number + expansionPotential: number + flexibility: number + [key: string]: number +} + +export interface SoftFactorPreferences { + minPrestige?: number + minAccessibility?: number + requireParking?: boolean + maxPublicTransportMinutes?: number + preferredEsgRating?: string + requireHighVisibility?: boolean +} + +export interface Need { + id: string + companyName: string + contactName?: string + assetType: AssetType + requiredArea: AreaRange + preferredLocations: string[] + excludedLocations?: string[] + budgetRange: BudgetRange + timing: Timing + mustCriteriaText?: string[] + softFactors?: SoftFactorPreferences + weightingProfile: WeightingProfile + confidenceInCriteria: number + extractedFromText?: string + notes?: string + organizationId?: string + createdAt: string + updatedAt: string +} + +export type CreateNeedInput = Omit +export type UpdateNeedInput = Partial diff --git a/src/domain/property.ts b/src/domain/property.ts new file mode 100644 index 0000000..e615680 --- /dev/null +++ b/src/domain/property.ts @@ -0,0 +1,73 @@ +import type { AssetType, ResultType, AvailabilityStatus, DataFreshness, RiskLevel } from './enums' + +export interface Location { + city: string + district?: string + canton?: string + country: string + coordinates?: { + lat: number + lng: number + } +} + +export interface Address { + street: string + houseNumber: string + postalCode: string + city: string + country: string +} + +export interface SoftFactors { + prestige?: number + accessibility?: number + visibilityScore?: number + talentAccess?: number + esgRating?: string + passerbyFrequency?: 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH' + parkingSpots?: number + publicTransportMinutes?: number + infrastructureNotes?: string +} + +export interface DataQuality { + score: number + missingCriticalFields: string[] + missingOptionalFields: string[] + lastVerifiedAt?: string + freshness: DataFreshness + warnings: string[] +} + +export interface Property { + id: string + title: string + assetType: AssetType + resultType: ResultType + location: Location + address: Address + areaSqm: number + rentPricePerSqm: number + totalRentMonthly?: number + availabilityDate: string + availabilityStatus: AvailabilityStatus + sourceType: string + sourceUrl?: string + confidenceScore: number + dataQuality: DataQuality + softFactors?: SoftFactors + floorLevel?: number + expansionPotentialSqm?: number + contractDurationMonths?: number + ancillaryCosts?: number + riskLevel?: RiskLevel + description?: string + images?: string[] + organizationId?: string + createdAt: string + updatedAt: string +} + +export type CreatePropertyInput = Omit +export type UpdatePropertyInput = Partial diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..a5002d3 --- /dev/null +++ b/src/index.css @@ -0,0 +1,4 @@ +@layer theme, base, components, utilities; + +@import "tailwindcss/theme.css" layer(theme); +@import "tailwindcss/utilities.css" layer(utilities); diff --git a/src/lib/theme.ts b/src/lib/theme.ts new file mode 100644 index 0000000..915fc70 --- /dev/null +++ b/src/lib/theme.ts @@ -0,0 +1,108 @@ +import { createTheme } from '@mui/material/styles' + +export const theme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#1e3a5f', + light: '#2d5290', + dark: '#142a45', + contrastText: '#ffffff', + }, + secondary: { + main: '#2d6a8f', + light: '#4a8ab0', + dark: '#1e4d68', + contrastText: '#ffffff', + }, + background: { + default: '#f4f6f9', + paper: '#ffffff', + }, + text: { + primary: '#1a2332', + secondary: '#4a5568', + }, + error: { main: '#c0392b' }, + warning: { main: '#d97706' }, + success: { main: '#1a7a4a' }, + info: { main: '#2563eb' }, + divider: '#e2e8f0', + }, + typography: { + fontFamily: '"Inter", "Segoe UI", system-ui, -apple-system, sans-serif', + h1: { fontSize: '1.75rem', fontWeight: 700, letterSpacing: '-0.02em' }, + h2: { fontSize: '1.5rem', fontWeight: 600, letterSpacing: '-0.01em' }, + h3: { fontSize: '1.25rem', fontWeight: 600 }, + h4: { fontSize: '1.125rem', fontWeight: 600 }, + h5: { fontSize: '1rem', fontWeight: 600 }, + h6: { fontSize: '0.9375rem', fontWeight: 600 }, + subtitle1: { fontSize: '0.875rem', fontWeight: 600, lineHeight: 1.5 }, + subtitle2: { fontSize: '0.8125rem', fontWeight: 600, lineHeight: 1.4 }, + body1: { fontSize: '0.875rem', lineHeight: 1.6 }, + body2: { fontSize: '0.8125rem', lineHeight: 1.5 }, + caption: { fontSize: '0.75rem', lineHeight: 1.4, color: '#64748b' }, + overline: { fontSize: '0.6875rem', fontWeight: 600, letterSpacing: '0.08em', textTransform: 'uppercase' }, + button: { fontSize: '0.8125rem', fontWeight: 600, textTransform: 'none' }, + }, + shape: { borderRadius: 6 }, + shadows: [ + 'none', + '0 1px 2px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.08)', + '0 1px 4px rgba(0,0,0,0.06), 0 2px 6px rgba(0,0,0,0.08)', + '0 2px 6px rgba(0,0,0,0.07), 0 4px 10px rgba(0,0,0,0.08)', + '0 3px 8px rgba(0,0,0,0.08), 0 6px 14px rgba(0,0,0,0.08)', + '0 4px 10px rgba(0,0,0,0.08), 0 8px 18px rgba(0,0,0,0.08)', + '0 5px 12px rgba(0,0,0,0.09)', '0 6px 14px rgba(0,0,0,0.09)', + '0 7px 16px rgba(0,0,0,0.09)', '0 8px 18px rgba(0,0,0,0.09)', + '0 9px 20px rgba(0,0,0,0.09)', '0 10px 22px rgba(0,0,0,0.10)', + '0 11px 24px rgba(0,0,0,0.10)', '0 12px 26px rgba(0,0,0,0.10)', + '0 13px 28px rgba(0,0,0,0.10)', '0 14px 30px rgba(0,0,0,0.10)', + '0 15px 32px rgba(0,0,0,0.10)', '0 16px 34px rgba(0,0,0,0.10)', + '0 17px 36px rgba(0,0,0,0.10)', '0 18px 38px rgba(0,0,0,0.10)', + '0 19px 40px rgba(0,0,0,0.10)', '0 20px 42px rgba(0,0,0,0.10)', + '0 21px 44px rgba(0,0,0,0.10)', '0 22px 46px rgba(0,0,0,0.10)', + '0 24px 50px rgba(0,0,0,0.12)', + ], + components: { + MuiCard: { + defaultProps: { elevation: 1 }, + styleOverrides: { + root: { + border: '1px solid #e2e8f0', + '&:hover': { boxShadow: '0 2px 8px rgba(0,0,0,0.10)' }, + transition: 'box-shadow 0.15s ease', + }, + }, + }, + MuiButton: { + defaultProps: { disableElevation: true }, + styleOverrides: { + root: { borderRadius: 6, padding: '6px 16px' }, + containedPrimary: { + background: '#1e3a5f', + '&:hover': { background: '#142a45' }, + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { borderRadius: 4, fontSize: '0.75rem', fontWeight: 600, height: 22 }, + }, + }, + MuiTableCell: { + styleOverrides: { + head: { fontWeight: 600, fontSize: '0.75rem', color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.05em', padding: '8px 12px' }, + body: { fontSize: '0.8125rem', padding: '10px 12px' }, + }, + }, + MuiTooltip: { + defaultProps: { arrow: true }, + }, + MuiPaper: { + styleOverrides: { + root: { backgroundImage: 'none' }, + }, + }, + }, +}) diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..48a6589 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,32 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router' +import { StyledEngineProvider, ThemeProvider, CssBaseline } from '@mui/material' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { theme } from './lib/theme' +import './index.css' +import App from './App.tsx' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 5 * 60 * 1000, + retry: 1, + }, + }, +}) + +createRoot(document.getElementById('root')!).render( + + + + + + + + + + + + , +) diff --git a/src/mock-data/futureSignals.ts b/src/mock-data/futureSignals.ts new file mode 100644 index 0000000..fa33339 --- /dev/null +++ b/src/mock-data/futureSignals.ts @@ -0,0 +1,84 @@ +import { SignalType, RiskLevel } from '../domain/enums' +import type { FutureSignal } from '../domain/futureSignal' + +export const mockFutureSignals: FutureSignal[] = [ + { + id: 'signal-001', + signalType: SignalType.EXPANSION, + companyName: 'DataCloud Systems AG', + locationHint: 'Zürich-West / Technopark', + areaSqmEstimate: 600, + probability: 0.72, + confidenceScore: 0.68, + timeHorizonMonths: 10, + source: { + type: 'JOB_POSTING', + url: 'https://example.com/jobs/datacloud', + publishedAt: '2025-04-20', + credibility: 'MEDIUM', + }, + sensitivityLevel: 'INTERNAL', + disclaimer: 'Dieses Signal basiert auf AI-Analyse öffentlicher Daten (Stelleninserate, Presseberichte). Es handelt sich um ein probabilistisches Signal, kein bestätigtes Objekt.', + riskLevel: RiskLevel.MEDIUM, + marketIndicator: 'Tech-Sektor Stellenwachstum +38% YoY', + relevanceScore: 0.76, + isVerified: false, + expiresAt: '2026-03-01', + organizationId: 'org-wincasa', + createdAt: '2025-05-01T07:00:00Z', + updatedAt: '2025-05-10T07:00:00Z', + }, + { + id: 'signal-002', + signalType: SignalType.POSSIBLE_MOVE_OUT, + companyName: 'Helvetia Produktion GmbH', + propertyId: 'prop-006', + locationHint: 'Reinach BL, Industriezone Nord', + areaSqmEstimate: 3200, + probability: 0.48, + confidenceScore: 0.44, + timeHorizonMonths: 13, + source: { + type: 'PRESS', + url: 'https://example.com/news/helvetia-restrukturierung', + publishedAt: '2025-03-15', + credibility: 'HIGH', + }, + sensitivityLevel: 'CONFIDENTIAL', + disclaimer: 'Dieses Signal basiert auf Pressemeldungen zu Restrukturierungsplänen. Kein bestätigter Auszug. Vertraulich behandeln.', + riskLevel: RiskLevel.HIGH, + marketIndicator: 'Restrukturierungsankündigung Mutterkonzern', + relevanceScore: 0.61, + isVerified: false, + expiresAt: '2026-06-01', + organizationId: 'org-wincasa', + createdAt: '2025-05-03T08:00:00Z', + updatedAt: '2025-05-10T08:00:00Z', + }, + { + id: 'signal-003', + signalType: SignalType.CONSTRUCTION_PROJECT, + locationHint: 'Bern, Wankdorf', + areaSqmEstimate: 4500, + probability: 0.85, + confidenceScore: 0.80, + timeHorizonMonths: 24, + source: { + type: 'CONSTRUCTION_PERMIT', + publishedAt: '2025-02-10', + credibility: 'HIGH', + }, + sensitivityLevel: 'PUBLIC', + disclaimer: 'Baubewilligung öffentlich eingesehen. Fertigstellung gemäss Baugesuch Q1 2027. Vermietungsstart noch offen.', + riskLevel: RiskLevel.LOW, + marketIndicator: 'Neubau Büro/Gewerbeflächen Wankdorf West', + relevanceScore: 0.82, + isVerified: true, + verifiedBy: 'admin@ideal-sharing.ch', + verifiedAt: '2025-05-05T09:00:00Z', + expiresAt: '2027-03-01', + organizationId: 'org-wincasa', + createdAt: '2025-02-12T10:00:00Z', + updatedAt: '2025-05-05T09:00:00Z', + }, +] diff --git a/src/mock-data/index.ts b/src/mock-data/index.ts new file mode 100644 index 0000000..e7395db --- /dev/null +++ b/src/mock-data/index.ts @@ -0,0 +1,4 @@ +export { mockProperties } from './properties' +export { mockNeeds } from './needs' +export { mockMatches } from './matches' +export { mockFutureSignals } from './futureSignals' diff --git a/src/mock-data/matches.ts b/src/mock-data/matches.ts new file mode 100644 index 0000000..d07e068 --- /dev/null +++ b/src/mock-data/matches.ts @@ -0,0 +1,141 @@ +import { MatchStrength, RiskLevel } from '../domain/enums' +import type { Match } from '../domain/match' + +export const mockMatches: Match[] = [ + { + id: 'match-001', + propertyId: 'prop-001', + needId: 'need-001', + matchScore: 88, + matchStrength: MatchStrength.STRONG, + scoreBreakdown: { + hardMatchScore: 92, + softFactorScore: 85, + confidenceModifier: 0.97, + dataQualityModifier: 0.92, + totalScore: 88, + }, + positiveFactors: [ + { criterion: 'Fläche', weight: 0.20, score: 95, contribution: 19, explanation: '850m² liegt im Zielkorridor (600–1000m²)' }, + { criterion: 'ÖV-Anbindung', weight: 0.20, score: 90, contribution: 18, explanation: '4 Min. zur S-Bahn, Kriterium erfüllt' }, + { criterion: 'Standort', weight: 0.15, score: 88, contribution: 13.2, explanation: 'Zürich-West trifft bevorzugte Lage' }, + ], + negativeFactors: [ + { criterion: 'Mietpreis', weight: 0.15, score: 76, contribution: 11.4, explanation: 'CHF 38/m² liegt über Wunschbudget von CHF 35/m²' }, + ], + tradeoffs: [ + { criterion: 'Budget', concern: 'Mietpreis 8% über Budget-Maximum', severity: 'MEDIUM', mitigation: 'Verhandlungspotenzial vorhanden – Objekt leer seit 3 Monaten' }, + ], + explainabilitySummary: 'Starker Match aufgrund idealer Lage und Fläche. Einziger Vorbehalt ist der Mietpreis, der knapp über dem Budget liegt, aber verhandelbar erscheint.', + confidenceLevel: 0.92, + riskLevel: RiskLevel.LOW, + uncertaintyIndicators: [], + alternativeStrategies: [ + { title: 'Kleinere Einheit im gleichen Gebäude', description: '650m² verfügbar ab Q3 2025, CHF 35/m²', expectedScore: 82 }, + ], + organizationId: 'org-wincasa', + createdAt: '2025-05-10T08:00:00Z', + updatedAt: '2025-05-10T08:00:00Z', + }, + { + id: 'match-002', + propertyId: 'prop-004', + needId: 'need-001', + matchScore: 64, + matchStrength: MatchStrength.MODERATE, + scoreBreakdown: { + hardMatchScore: 78, + softFactorScore: 62, + confidenceModifier: 0.68, + dataQualityModifier: 0.55, + totalScore: 64, + }, + positiveFactors: [ + { criterion: 'Fläche', weight: 0.20, score: 88, contribution: 17.6, explanation: '1150m² liegt im erweiterten Korridor' }, + { criterion: 'Standort', weight: 0.20, score: 80, contribution: 16, explanation: 'Zürich Kreis 4 nahe bevorzugten Lagen' }, + ], + negativeFactors: [ + { criterion: 'Datenqualität', weight: 0.10, score: 40, contribution: 4, explanation: 'Mehrere kritische Felder fehlen – Verlässlichkeit eingeschränkt' }, + { criterion: 'Mietpreis', weight: 0.15, score: 55, contribution: 8.25, explanation: 'CHF 52/m² deutlich über Budget' }, + ], + tradeoffs: [ + { criterion: 'Datenqualität', concern: 'Externe Quelle – Mietpreis und Verfügbarkeit nicht bestätigt', severity: 'HIGH', mitigation: 'Direkte Anfrage beim Anbieter empfohlen' }, + { criterion: 'Budget', concern: 'Mietpreis 30% über Budget-Maximum', severity: 'HIGH' }, + ], + explainabilitySummary: 'Moderater Match – Fläche und Lage passen, aber Mietpreis und Datenqualität sind kritische Vorbehalte. Nur mit Preisverhandlung und Verifikation sinnvoll.', + confidenceLevel: 0.58, + riskLevel: RiskLevel.MEDIUM, + uncertaintyIndicators: ['Daten aus Drittquelle unvollständig', 'Mietpreis nicht verifiziert'], + organizationId: 'org-wincasa', + createdAt: '2025-05-10T08:05:00Z', + updatedAt: '2025-05-10T08:05:00Z', + }, + { + id: 'match-003', + propertyId: 'prop-002', + needId: 'need-002', + matchScore: 91, + matchStrength: MatchStrength.STRONG, + scoreBreakdown: { + hardMatchScore: 94, + softFactorScore: 89, + confidenceModifier: 0.99, + dataQualityModifier: 0.96, + totalScore: 91, + }, + positiveFactors: [ + { criterion: 'Fläche', weight: 0.25, score: 96, contribution: 24, explanation: '2400m² im Zielkorridor (1500–4000m²)' }, + { criterion: 'Autobahnanschluss', weight: 0.20, score: 92, contribution: 18.4, explanation: 'A2-Anschluss ca. 4 Min., Kriterium erfüllt' }, + { criterion: 'Budget', weight: 0.20, score: 88, contribution: 17.6, explanation: 'CHF 14/m² liegt unter Maximum von CHF 18/m²' }, + { criterion: 'Datenqualität', weight: 0.10, score: 96, contribution: 9.6, explanation: 'Verifiziertes Portfolioobjekt, alle Felder vollständig' }, + ], + negativeFactors: [ + { criterion: 'Erweiterungspotenzial', weight: 0.05, score: 70, contribution: 3.5, explanation: '800m² Erweiterung möglich, aber begrenzt' }, + ], + tradeoffs: [], + explainabilitySummary: 'Sehr starker Match. Fläche, Anbindung und Budget passen exzellent. Hohe Datenqualität aus internem Portfolio bestätigt Verlässlichkeit.', + confidenceLevel: 0.96, + riskLevel: RiskLevel.LOW, + uncertaintyIndicators: [], + organizationId: 'org-wincasa', + createdAt: '2025-05-10T08:10:00Z', + updatedAt: '2025-05-10T08:10:00Z', + }, + { + id: 'match-004', + propertyId: 'prop-006', + needId: 'need-002', + matchScore: 47, + matchStrength: MatchStrength.WEAK, + scoreBreakdown: { + hardMatchScore: 70, + softFactorScore: 40, + confidenceModifier: 0.48, + dataQualityModifier: 0.30, + totalScore: 47, + }, + positiveFactors: [ + { criterion: 'Fläche', weight: 0.25, score: 85, contribution: 21.25, explanation: '3200m² im erweiterten Zielkorridor' }, + { criterion: 'Standort', weight: 0.20, score: 72, contribution: 14.4, explanation: 'Reinach BL ist eine bevorzugte Region' }, + ], + negativeFactors: [ + { criterion: 'Confidence', weight: 0.15, score: 20, contribution: 3, explanation: 'Nur 48% Signalwahrscheinlichkeit – kein bestätigtes Objekt' }, + { criterion: 'Datenqualität', weight: 0.10, score: 15, contribution: 1.5, explanation: 'Mehrere kritische Felder fehlen, Schätzwerte' }, + { criterion: 'Timing', weight: 0.15, score: 50, contribution: 7.5, explanation: 'Verfügbarkeit erst Q2 2026, spät für Anforderung' }, + ], + tradeoffs: [ + { criterion: 'Verfügbarkeit', concern: 'Probabilistisches Future-Signal – keine Garantie auf Verfügbarkeit', severity: 'HIGH', mitigation: 'Für Monitoring-Watchlist geeignet' }, + { criterion: 'Daten', concern: 'Mietpreis geschätzt, Andienung nicht bestätigt', severity: 'HIGH' }, + ], + explainabilitySummary: 'Schwacher Match aufgrund hoher Unsicherheit. Das Signal ist interessant als Frühindikator, aber nicht als aktive Option geeignet. Empfehlung: Watchlist.', + confidenceLevel: 0.38, + riskLevel: RiskLevel.HIGH, + uncertaintyIndicators: ['Probabilistisches Signal ohne Bestätigung', 'Mietpreis geschätzt', 'Verfügbarkeitsdatum unsicher'], + alternativeStrategies: [ + { title: 'Signal beobachten', description: 'Als Future-Availability-Signal auf Watchlist setzen und in 3 Monaten neu evaluieren' }, + ], + organizationId: 'org-wincasa', + createdAt: '2025-05-10T08:15:00Z', + updatedAt: '2025-05-10T08:15:00Z', + }, +] diff --git a/src/mock-data/needs.ts b/src/mock-data/needs.ts new file mode 100644 index 0000000..00b0f9b --- /dev/null +++ b/src/mock-data/needs.ts @@ -0,0 +1,76 @@ +import { AssetType } from '../domain/enums' +import type { Need } from '../domain/need' + +export const mockNeeds: Need[] = [ + { + id: 'need-001', + companyName: 'Innovatech AG', + contactName: 'Sandra Meier', + assetType: AssetType.OFFICE, + requiredArea: { min: 600, max: 1000 }, + preferredLocations: ['Zürich', 'Zürich-West', 'Zürich Kreis 5'], + excludedLocations: [], + budgetRange: { maxPerSqm: 45, maxMonthlyTotal: 40000, currency: 'CHF' }, + timing: { + earliestMoveIn: '2025-08-01', + latestMoveIn: '2026-01-01', + contractDurationMonths: 60, + flexibleTiming: true, + }, + mustCriteriaText: ['ÖV-Anbindung < 5 Min', 'Mindestfläche 600m²', 'Ausbaugrad modern'], + softFactors: { + minPrestige: 70, + minAccessibility: 80, + requireParking: false, + maxPublicTransportMinutes: 6, + }, + weightingProfile: { + area: 0.20, + location: 0.20, + budget: 0.15, + timing: 0.15, + prestige: 0.10, + accessibility: 0.10, + expansionPotential: 0.05, + flexibility: 0.05, + }, + confidenceInCriteria: 0.88, + extractedFromText: 'Wir suchen moderne Büroflächen in Zürich-West, ca. 700–900m², Budget max CHF 42/m², Bezug Herbst 2025.', + organizationId: 'org-wincasa', + createdAt: '2025-04-15T10:00:00Z', + updatedAt: '2025-05-01T09:00:00Z', + }, + { + id: 'need-002', + companyName: 'Schweizer Logistik GmbH', + contactName: 'Thomas Brun', + assetType: AssetType.LOGISTICS, + requiredArea: { min: 1500, max: 4000 }, + preferredLocations: ['Basel', 'Muttenz', 'Pratteln', 'Reinach BL'], + budgetRange: { maxPerSqm: 18, currency: 'CHF' }, + timing: { + earliestMoveIn: '2025-09-01', + latestMoveIn: '2026-06-01', + contractDurationMonths: 36, + flexibleTiming: false, + }, + mustCriteriaText: ['Autobahnanschluss < 5 Min', 'Rampe / Andienung', 'Hallenhöhe min 6m'], + softFactors: { + requireParking: true, + }, + weightingProfile: { + area: 0.25, + location: 0.20, + budget: 0.20, + timing: 0.15, + prestige: 0.02, + accessibility: 0.10, + expansionPotential: 0.05, + flexibility: 0.03, + }, + confidenceInCriteria: 0.94, + organizationId: 'org-wincasa', + createdAt: '2025-03-20T14:00:00Z', + updatedAt: '2025-04-10T11:00:00Z', + }, +] diff --git a/src/mock-data/properties.ts b/src/mock-data/properties.ts new file mode 100644 index 0000000..6b32ab2 --- /dev/null +++ b/src/mock-data/properties.ts @@ -0,0 +1,192 @@ +import { AssetType, ResultType, AvailabilityStatus, DataFreshness, RiskLevel } from '../domain/enums' +import type { Property } from '../domain/property' + +export const mockProperties: Property[] = [ + // --- VERIFIED_PORTFOLIO --- + { + id: 'prop-001', + title: 'Bürofläche Zollstrasse 12', + assetType: AssetType.OFFICE, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Zürich', district: 'Zürich-West', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3882, lng: 8.5132 } }, + address: { street: 'Zollstrasse', houseNumber: '12', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 850, + rentPricePerSqm: 38, + totalRentMonthly: 32300, + availabilityDate: '2025-09-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.97, + dataQuality: { + score: 0.92, + missingCriticalFields: [], + missingOptionalFields: ['expansionPotentialSqm'], + lastVerifiedAt: '2025-04-28', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 78, + accessibility: 90, + visibilityScore: 65, + talentAccess: 85, + parkingSpots: 12, + publicTransportMinutes: 4, + }, + floorLevel: 3, + contractDurationMonths: 60, + ancillaryCosts: 5.5, + riskLevel: RiskLevel.LOW, + organizationId: 'org-wincasa', + createdAt: '2025-01-10T08:00:00Z', + updatedAt: '2025-04-28T10:30:00Z', + }, + { + id: 'prop-002', + title: 'Lagerfläche Hardstrasse 44', + assetType: AssetType.LOGISTICS, + resultType: ResultType.VERIFIED_PORTFOLIO, + location: { city: 'Basel', district: 'Kleinhüningen', canton: 'BS', country: 'CH', coordinates: { lat: 47.5736, lng: 7.5946 } }, + address: { street: 'Hardstrasse', houseNumber: '44', postalCode: '4057', city: 'Basel', country: 'CH' }, + areaSqm: 2400, + rentPricePerSqm: 14, + totalRentMonthly: 33600, + availabilityDate: '2025-07-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, + sourceType: 'ERP_IMPORT', + confidenceScore: 0.99, + dataQuality: { + score: 0.96, + missingCriticalFields: [], + missingOptionalFields: [], + lastVerifiedAt: '2025-05-05', + freshness: DataFreshness.FRESH, + warnings: [], + }, + softFactors: { + prestige: 40, + accessibility: 88, + parkingSpots: 30, + publicTransportMinutes: 12, + }, + floorLevel: 0, + expansionPotentialSqm: 800, + contractDurationMonths: 36, + ancillaryCosts: 3.0, + riskLevel: RiskLevel.LOW, + organizationId: 'org-wincasa', + createdAt: '2024-11-20T09:00:00Z', + updatedAt: '2025-05-05T11:00:00Z', + }, + + // --- EXTERNAL_MARKET --- + { + id: 'prop-003', + title: 'Retail-Fläche Bahnhofstrasse 88', + assetType: AssetType.RETAIL, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Bern', district: 'Innenstadt', canton: 'BE', country: 'CH', coordinates: { lat: 46.9483, lng: 7.4474 } }, + address: { street: 'Bahnhofstrasse', houseNumber: '88', postalCode: '3011', city: 'Bern', country: 'CH' }, + areaSqm: 320, + rentPricePerSqm: 95, + totalRentMonthly: 30400, + availabilityDate: '2025-08-15', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'MATCHOFFICE_SCRAPE', + sourceUrl: 'https://example.com/listing/prop-003', + confidenceScore: 0.71, + dataQuality: { + score: 0.62, + missingCriticalFields: ['contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts', 'floorLevel'], + lastVerifiedAt: '2025-04-10', + freshness: DataFreshness.STALE, + warnings: ['Mietpreis nicht bestätigt', 'Verfügbarkeit nicht verifiziert'], + }, + softFactors: { + prestige: 92, + visibilityScore: 98, + passerbyFrequency: 'VERY_HIGH', + publicTransportMinutes: 2, + }, + riskLevel: RiskLevel.MEDIUM, + createdAt: '2025-02-15T14:00:00Z', + updatedAt: '2025-04-10T09:00:00Z', + }, + { + id: 'prop-004', + title: 'Gemischte Gewerbeeinheit Europaallee', + assetType: AssetType.MIXED, + resultType: ResultType.EXTERNAL_MARKET, + location: { city: 'Zürich', district: 'Kreis 4', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3775, lng: 8.5398 } }, + address: { street: 'Europaallee', houseNumber: '21', postalCode: '8004', city: 'Zürich', country: 'CH' }, + areaSqm: 1150, + rentPricePerSqm: 52, + totalRentMonthly: 59800, + availabilityDate: '2025-10-01', + availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, + sourceType: 'IMMOSCOUT_SCRAPE', + confidenceScore: 0.68, + dataQuality: { + score: 0.55, + missingCriticalFields: ['contractDurationMonths', 'ancillaryCosts'], + missingOptionalFields: ['softFactors'], + lastVerifiedAt: '2025-03-20', + freshness: DataFreshness.STALE, + warnings: ['Daten aus Drittquelle – nicht verifiziert'], + }, + riskLevel: RiskLevel.MEDIUM, + createdAt: '2025-03-01T10:00:00Z', + updatedAt: '2025-03-20T15:00:00Z', + }, + + // --- FUTURE_AVAILABILITY --- + { + id: 'prop-005', + title: 'Bürofläche Technoparkstrasse (Signal: Expansion)', + assetType: AssetType.OFFICE, + resultType: ResultType.FUTURE_AVAILABILITY, + location: { city: 'Zürich', district: 'Technopark', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3923, lng: 8.5142 } }, + address: { street: 'Technoparkstrasse', houseNumber: '1', postalCode: '8005', city: 'Zürich', country: 'CH' }, + areaSqm: 600, + rentPricePerSqm: 42, + availabilityDate: '2026-03-01', + availabilityStatus: AvailabilityStatus.FUTURE_SIGNAL, + sourceType: 'AI_SIGNAL', + confidenceScore: 0.55, + dataQuality: { + score: 0.38, + missingCriticalFields: ['totalRentMonthly', 'contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts', 'floorLevel', 'parkingSpots'], + freshness: DataFreshness.FRESH, + warnings: ['Probabilistisches Signal – kein bestätigtes Objekt', 'Daten nicht verifiziert'], + }, + riskLevel: RiskLevel.HIGH, + createdAt: '2025-05-01T07:00:00Z', + updatedAt: '2025-05-10T07:00:00Z', + }, + { + id: 'prop-006', + title: 'Produktionsfläche Reinach (Signal: möglicher Auszug)', + assetType: AssetType.PRODUCTION, + resultType: ResultType.FUTURE_AVAILABILITY, + location: { city: 'Reinach', district: 'Industriezone Nord', canton: 'BL', country: 'CH', coordinates: { lat: 47.4978, lng: 7.5933 } }, + address: { street: 'Industriestrasse', houseNumber: '18', postalCode: '4153', city: 'Reinach', country: 'CH' }, + areaSqm: 3200, + rentPricePerSqm: 11, + availabilityDate: '2026-06-01', + availabilityStatus: AvailabilityStatus.FUTURE_SIGNAL, + sourceType: 'AI_SIGNAL', + confidenceScore: 0.48, + dataQuality: { + score: 0.30, + missingCriticalFields: ['totalRentMonthly', 'rentPricePerSqm', 'contractDurationMonths'], + missingOptionalFields: ['ancillaryCosts', 'softFactors'], + freshness: DataFreshness.FRESH, + warnings: ['Probabilistisches Signal – kein bestätigtes Objekt', 'Mietpreis geschätzt'], + }, + riskLevel: RiskLevel.HIGH, + createdAt: '2025-05-03T08:00:00Z', + updatedAt: '2025-05-10T08:00:00Z', + }, +] diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx new file mode 100644 index 0000000..27f0036 --- /dev/null +++ b/src/pages/Home.tsx @@ -0,0 +1,43 @@ +import { Container, Typography, Button, Card, CardContent, Chip } from '@mui/material' +import HomeIcon from '@mui/icons-material/Home' +import SearchIcon from '@mui/icons-material/Search' + +export default function Home() { + return ( + +
+ + Property Match + + + Find your ideal property + +
+ +
+ + +
+ +
+ {(['Buy', 'Rent', 'Invest'] as const).map((category) => ( + + + + + {category} a Property + + + Browse listings available for {category.toLowerCase()} in your area. + + + + ))} +
+
+ ) +} diff --git a/src/pages/demand/AISearch.tsx b/src/pages/demand/AISearch.tsx new file mode 100644 index 0000000..8137c95 --- /dev/null +++ b/src/pages/demand/AISearch.tsx @@ -0,0 +1,338 @@ +import { useState } from 'react' +import { + Box, + Button, + Card, + Chip, + TextField, + Typography, + CircularProgress, + Alert, + Stack, + Divider, +} from '@mui/material' +import { Sparkles, ArrowRight } from 'lucide-react' +import { useNavigate } from 'react-router' +import { MockupAIServiceProvider, type CriteriaExtractionResult } from '../../services/aiService' + +type Step = 'input' | 'extracting' | 'review' | 'done' + +const EXAMPLE_QUERIES = [ + 'Büro Zürich 500-800m²', + 'Logistik Basel 2000m²', + 'Retail Bern Innenstadt', +] + +function ConfidenceColor(score: number): string { + if (score >= 0.8) return '#1a7a4a' + if (score >= 0.6) return '#d97706' + return '#c0392b' +} + +export default function AISearch() { + const navigate = useNavigate() + const [step, setStep] = useState('input') + const [inputText, setInputText] = useState('') + const [extractedCriteria, setExtractedCriteria] = useState(null) + const [followUpAnswers, setFollowUpAnswers] = useState>({}) + + const handleAnalyze = async () => { + setStep('extracting') + await new Promise(r => setTimeout(r, 1500)) + const result = await MockupAIServiceProvider.extractCriteria(inputText) + setExtractedCriteria(result) + setStep('review') + } + + const handleStartSearch = () => { + navigate('/demand/results', { state: { needId: 'need-001' } }) + } + + return ( + + {/* Page Header */} + + + AI Bedarfsanalyse + + + Beschreiben Sie Ihren Flächenbedarf in natürlicher Sprache + + + + + {/* Step 1: Input */} + {step === 'input' && ( + + + + Flächenbedarf beschreiben + + setInputText(e.target.value)} + inputProps={{ maxLength: 2000 }} + sx={{ mb: 1 }} + /> + + {inputText.length}/2000 + + + + Die KI extrahiert automatisch Kriterien, Standortpräferenzen und Budget aus Ihrer Beschreibung. + + + + {/* Example Queries */} + + + Beispiele: + + + {EXAMPLE_QUERIES.map(q => ( + setInputText(q)} + sx={{ cursor: 'pointer' }} + /> + ))} + + + + )} + + {/* Step 2: Extracting */} + {step === 'extracting' && ( + + + + KI analysiert Ihren Bedarf... + + + )} + + {/* Step 3: Review */} + {step === 'review' && extractedCriteria && ( + + + {/* Left: Extracted Criteria */} + + + Extrahierte Kriterien + + + + {/* Confidence badge */} + + Gesamtkonfidenz: + + + + + + {/* Criteria items */} + {extractedCriteria.extractedCriteria.requiredArea && ( + + + Flächenbedarf + + + {extractedCriteria.extractedCriteria.requiredArea.min}– + {extractedCriteria.extractedCriteria.requiredArea.max} m² + + + )} + + {extractedCriteria.extractedCriteria.budgetRange && ( + + + Budget + + + max. {extractedCriteria.extractedCriteria.budgetRange.maxPerSqm}{' '} + {extractedCriteria.extractedCriteria.budgetRange.currency}/m² + + + )} + + {extractedCriteria.extractedCriteria.companyName && ( + + + Unternehmen + + + {extractedCriteria.extractedCriteria.companyName} + + + )} + + {extractedCriteria.extractedCriteria.preferredLocations && extractedCriteria.extractedCriteria.preferredLocations.length > 0 && ( + + + Standort + + + {extractedCriteria.extractedCriteria.preferredLocations.join(', ')} + + + )} + + {extractedCriteria.extractedCriteria.timing && ( + + + Verfügbarkeit + + + ab {extractedCriteria.extractedCriteria.timing.earliestMoveIn} + + + )} + + {extractedCriteria.extractedCriteria.assetType && ( + + + Objekttyp + + + {extractedCriteria.extractedCriteria.assetType} + + + )} + + + {/* Assumptions */} + {extractedCriteria.assumptions.length > 0 && ( + + + Annahmen der KI + + + {extractedCriteria.assumptions.map((a, i) => ( + + {a} + + ))} + + + )} + + {/* Missing Fields */} + {extractedCriteria.missingFields.length > 0 && ( + + + Fehlende Informationen + + + {extractedCriteria.missingFields.map(f => ( + + ))} + + + )} + + + {/* Right: Follow-up Questions */} + + + Rückfragen der KI + + + Diese Fragen sind optional — verbessern jedoch die Trefferqualität. + + + + {extractedCriteria.followUpQuestions.map((q, i) => ( + + + {i + 1}. {q} + + + setFollowUpAnswers(prev => ({ ...prev, [i]: e.target.value })) + } + /> + + ))} + + + + + {/* Footer Actions */} + + + + + + )} + + + ) +} diff --git a/src/pages/demand/Compare.tsx b/src/pages/demand/Compare.tsx new file mode 100644 index 0000000..5189c2e --- /dev/null +++ b/src/pages/demand/Compare.tsx @@ -0,0 +1,320 @@ +import { + Box, + Button, + Card, + Chip, + Typography, + LinearProgress, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + CircularProgress, + Stack, +} from '@mui/material' +import { X } from 'lucide-react' +import { useQuery } from '@tanstack/react-query' +import { useNavigate } from 'react-router' +import { propertyService } from '../../services/propertyService' +import { useCompareStore } from '../../stores/compareStore' +import { EmptyState } from '../../components/ui' +import { ResultType, RiskLevel } from '../../domain/enums' +import type { Property } from '../../domain/property' + +function getResultTypeLabel(type: ResultType): string { + switch (type) { + case ResultType.VERIFIED_PORTFOLIO: return 'Verified Portfolio' + case ResultType.EXTERNAL_MARKET: return 'Marktinserat' + case ResultType.FUTURE_AVAILABILITY: return 'Zukunftssignal' + } +} + +function getResultTypeColor(type: ResultType): string { + switch (type) { + case ResultType.VERIFIED_PORTFOLIO: return '#1e3a5f' + case ResultType.EXTERNAL_MARKET: return '#d97706' + case ResultType.FUTURE_AVAILABILITY: return '#7c3aed' + } +} + +function getRiskColor(risk?: RiskLevel): 'success' | 'warning' | 'error' | 'default' { + if (!risk) return 'default' + if (risk === RiskLevel.LOW) return 'success' + if (risk === RiskLevel.MEDIUM) return 'warning' + return 'error' +} + +function getRiskLabel(risk?: RiskLevel): string { + if (!risk) return '–' + switch (risk) { + case RiskLevel.LOW: return 'Niedrig' + case RiskLevel.MEDIUM: return 'Mittel' + case RiskLevel.HIGH: return 'Hoch' + case RiskLevel.CRITICAL: return 'Kritisch' + } +} + +interface CompareRow { + label: string + getValue: (p: Property) => string | number | null + format?: (v: string | number | null, p: Property) => React.ReactNode + isHigherBetter?: boolean + isLowerBetter?: boolean +} + +function NumericCell({ value, isBest }: { value: React.ReactNode; isBest: boolean }) { + return ( + + {value} + + ) +} + +export default function Compare() { + const navigate = useNavigate() + const { compareTray, removeFromCompare, clearCompare } = useCompareStore() + + const { data: propResp, isLoading } = useQuery({ + queryKey: ['properties'], + queryFn: () => propertyService.getAll(), + }) + + const properties = propResp?.data ?? [] + const compareProperties = properties.filter(p => compareTray.includes(p.id)) + // Keep ordering same as tray + const orderedProperties = compareTray + .map(id => compareProperties.find(p => p.id === id)) + .filter((p): p is Property => p !== null) + + if (isLoading) { + return ( + + + + ) + } + + if (compareTray.length === 0) { + return ( + + + Vergleich + + + navigate('/demand/results') }} + /> + + + ) + } + + const rows: CompareRow[] = [ + { + label: 'Fläche (m²)', + getValue: p => p.areaSqm, + isHigherBetter: false, + }, + { + label: 'Miete/m² (CHF)', + getValue: p => p.rentPricePerSqm, + isLowerBetter: true, + }, + { + label: 'Gesamtmiete/Monat (CHF)', + getValue: p => p.totalRentMonthly ?? null, + format: (v) => v != null ? `${Number(v).toLocaleString('de-CH')} CHF` : , + isLowerBetter: true, + }, + { + label: 'Verfügbarkeit', + getValue: p => p.availabilityDate, + format: (v) => v ?? , + }, + { + label: 'Standort', + getValue: p => `${p.location.city}${p.location.district ? ', ' + p.location.district : ''}`, + }, + { + label: 'Datenqualität', + getValue: p => p.dataQuality.score, + format: (v, p) => ( + + + = 0.8 ? 'success' : p.dataQuality.score >= 0.6 ? 'warning' : 'error'} + /> + + {Math.round(p.dataQuality.score * 100)}% + + ), + isHigherBetter: true, + }, + { + label: 'Konfidenz', + getValue: p => p.confidenceScore, + format: (v) => v != null ? `${Math.round(Number(v) * 100)}%` : , + isHigherBetter: true, + }, + { + label: 'Risiko', + getValue: p => p.riskLevel ?? null, + format: (v, p) => ( + + ), + }, + { + label: 'Prestige', + getValue: p => p.softFactors?.prestige ?? null, + format: (v) => v != null ? String(v) : , + isHigherBetter: true, + }, + { + label: 'Erreichbarkeit', + getValue: p => p.softFactors?.accessibility ?? null, + format: (v) => v != null ? String(v) : , + isHigherBetter: true, + }, + { + label: 'ÖV-Minuten', + getValue: p => p.softFactors?.publicTransportMinutes ?? null, + format: (v) => v != null ? `${v} Min.` : , + isLowerBetter: true, + }, + { + label: 'Fehlende Pflichtfelder', + getValue: p => p.dataQuality.missingCriticalFields.length, + format: (v) => v != null ? String(v) : , + isLowerBetter: true, + }, + ] + + return ( + + {/* Page Header */} + + + Vergleich + + + + + + + + + + + + Kriterium + + {orderedProperties.map(p => ( + + + + {p.title} + + + + + + ))} + + + + + {rows.map(row => { + const values = orderedProperties.map(p => row.getValue(p)) + const numericValues = values + .map((v, i) => ({ v, i })) + .filter(x => x.v != null && typeof x.v === 'number') as { v: number; i: number }[] + + let bestIdx = -1 + if (numericValues.length > 1) { + if (row.isHigherBetter) { + bestIdx = numericValues.reduce((best, cur) => cur.v > best.v ? cur : best).i + } else if (row.isLowerBetter) { + bestIdx = numericValues.reduce((best, cur) => cur.v < best.v ? cur : best).i + } + } + + return ( + + + {row.label} + + {orderedProperties.map((p, idx) => { + const raw = row.getValue(p) + const displayValue = row.format + ? row.format(raw, p) + : raw != null + ? String(raw) + : + + const isBest = bestIdx === idx + return ( + + ) + })} + + ) + })} + +
+
+ + {/* Add more prompt */} + {orderedProperties.length < 3 && ( + + + + Weiteres Objekt hinzufügen + + Bis zu {3 - orderedProperties.length} weitere{orderedProperties.length < 2 ? 's' : ''} Objekt{orderedProperties.length < 2 ? '' : 'e'} möglich + + + + + + )} +
+
+ ) +} diff --git a/src/pages/demand/Results.tsx b/src/pages/demand/Results.tsx new file mode 100644 index 0000000..2c4dbdc --- /dev/null +++ b/src/pages/demand/Results.tsx @@ -0,0 +1,476 @@ +import { useState } from 'react' +import { + Box, + Button, + Card, + Chip, + Typography, + LinearProgress, + Stack, + Alert, + CircularProgress, + Divider, +} from '@mui/material' +import { + MapPin, + Maximize2, + Banknote, + Calendar, + Bookmark, + Columns2, +} from 'lucide-react' +import { useQuery } from '@tanstack/react-query' +import { useNavigate, useLocation } from 'react-router' +import { propertyService } from '../../services/propertyService' +import { matchService } from '../../services/matchService' +import { needService } from '../../services/needService' +import { ResultType, MatchStrength, RiskLevel } from '../../domain/enums' +import type { Property } from '../../domain/property' +import type { Match } from '../../domain/match' +import { useCompareStore } from '../../stores/compareStore' +import { EmptyState } from '../../components/ui' + +type FilterSource = ResultType | 'ALL' +type SortBy = 'score' | 'rent' | 'area' + +function getResultTypeLabel(type: ResultType): string { + switch (type) { + case ResultType.VERIFIED_PORTFOLIO: return 'Verified Portfolio' + case ResultType.EXTERNAL_MARKET: return 'Marktinserat' + case ResultType.FUTURE_AVAILABILITY: return 'Zukunftssignal' + } +} + +function getResultTypeColor(type: ResultType): string { + switch (type) { + case ResultType.VERIFIED_PORTFOLIO: return '#1e3a5f' + case ResultType.EXTERNAL_MARKET: return '#d97706' + case ResultType.FUTURE_AVAILABILITY: return '#7c3aed' + } +} + +function getMatchStrengthColor(strength: MatchStrength): string { + switch (strength) { + case MatchStrength.STRONG: return '#1a7a4a' + case MatchStrength.MODERATE: return '#d97706' + case MatchStrength.WEAK: return '#c0392b' + } +} + +function getRiskChipColor(risk: RiskLevel): 'success' | 'warning' | 'error' { + if (risk === RiskLevel.LOW) return 'success' + if (risk === RiskLevel.MEDIUM) return 'warning' + return 'error' +} + +function getRiskLabel(risk: RiskLevel): string { + switch (risk) { + case RiskLevel.LOW: return 'Niedriges Risiko' + case RiskLevel.MEDIUM: return 'Mittleres Risiko' + case RiskLevel.HIGH: return 'Hohes Risiko' + case RiskLevel.CRITICAL: return 'Kritisches Risiko' + } +} + +interface ResultCardProps { + property: Property + match: Match + onCompare: (id: string) => void + isInCompare: boolean +} + +function ResultCard({ property, match, onCompare, isInCompare }: ResultCardProps) { + const scoreColor = getMatchStrengthColor(match.matchStrength) + + return ( + + {/* Future signal warning */} + {property.resultType === ResultType.FUTURE_AVAILABILITY && ( + + Probabilistisches Signal – kein bestätigtes Objekt + + )} + + {/* Header row */} + + + + + {property.title} + + + + + {match.matchScore} + + /100 + + + + {/* Property details row */} + + + + + {property.location.city} + {property.location.district ? `, ${property.location.district}` : ''} + + + + + + {property.areaSqm} m² + + + + + + CHF {property.rentPricePerSqm}/m² + + + + + + {property.availabilityDate} + + + + + {/* Match factors section */} + + + {match.positiveFactors.length > 0 && ( + + + Positive Faktoren + + + {match.positiveFactors.slice(0, 3).map((f, i) => ( + + ))} + + + )} + + {match.tradeoffs.length > 0 && ( + + Abwägungen + {match.tradeoffs.slice(0, 2).map((t, i) => ( + + {t.criterion}: {t.concern} + + ))} + + )} + + {/* Confidence + Quality row */} + + + Konfidenz + = 0.8 ? '#1a7a4a' : match.confidenceLevel >= 0.6 ? '#d97706' : '#c0392b' }}> + {Math.round(match.confidenceLevel * 100)}% + + + + Datenqualität + + = 0.8 ? 'success' : + property.dataQuality.score >= 0.6 ? 'warning' : 'error' + } + /> + + + {Math.round(property.dataQuality.score * 100)}% + + + {property.riskLevel && ( + + )} + + + {/* Actions row */} + + + + + + + ) +} + +export default function Results() { + const navigate = useNavigate() + const location = useLocation() + const _needId = (location.state as { needId?: string } | null)?.needId ?? 'need-001' + + const [filterSource, setFilterSource] = useState('ALL') + const [sortBy, setSortBy] = useState('score') + + const { data: propResp, isLoading: propLoading } = useQuery({ + queryKey: ['properties'], + queryFn: () => propertyService.getAll(), + }) + const { data: matchResp, isLoading: matchLoading } = useQuery({ + queryKey: ['matches'], + queryFn: () => matchService.getAll(), + }) + const { data: needResp, isLoading: needLoading } = useQuery({ + queryKey: ['needs'], + queryFn: () => needService.getAll(), + }) + + const { addToCompare, removeFromCompare, clearCompare, isInCompare, compareTray } = useCompareStore() + + const properties = propResp?.data ?? [] + const matches = matchResp?.data ?? [] + const needs = needResp?.data ?? [] + const activeNeed = needs[0] + + const isLoading = propLoading || matchLoading || needLoading + + // Join matches with properties + const resultItems = matches + .map(match => { + const property = properties.find(p => p.id === match.propertyId) + return property ? { match, property } : null + }) + .filter((item): item is { match: Match; property: Property } => item !== null) + + // Filter by source + const filtered = filterSource === 'ALL' + ? resultItems + : resultItems.filter(item => item.property.resultType === filterSource) + + // Sort + const sorted = [...filtered].sort((a, b) => { + if (sortBy === 'score') return b.match.matchScore - a.match.matchScore + if (sortBy === 'rent') return a.property.rentPricePerSqm - b.property.rentPricePerSqm + if (sortBy === 'area') return b.property.areaSqm - a.property.areaSqm + return 0 + }) + + const verifiedCount = resultItems.filter(i => i.property.resultType === ResultType.VERIFIED_PORTFOLIO).length + const externalCount = resultItems.filter(i => i.property.resultType === ResultType.EXTERNAL_MARKET).length + const futureCount = resultItems.filter(i => i.property.resultType === ResultType.FUTURE_AVAILABILITY).length + + const handleToggleCompare = (id: string) => { + if (isInCompare(id)) { + removeFromCompare(id) + } else { + addToCompare(id) + } + } + + if (isLoading) { + return ( + + + + ) + } + + return ( + + {/* Page Header */} + + + + {sorted.length} Treffer gefunden + + + {verifiedCount} Verified · {externalCount} Extern · {futureCount} Signale + + + + + + {/* Active Need Banner */} + {activeNeed && ( + + + + + Aktive Suche: {activeNeed.companyName} + + + {activeNeed.assetType} · {activeNeed.requiredArea.min}–{activeNeed.requiredArea.max} m² ·{' '} + {activeNeed.preferredLocations.join(', ')} + + + + + + )} + + {/* Filter/Sort bar */} + + + + {(['ALL', ResultType.VERIFIED_PORTFOLIO, ResultType.EXTERNAL_MARKET, ResultType.FUTURE_AVAILABILITY] as FilterSource[]).map(source => { + const labels: Record = { + ALL: 'Alle', + [ResultType.VERIFIED_PORTFOLIO]: 'Verified Portfolio', + [ResultType.EXTERNAL_MARKET]: 'Marktinserate', + [ResultType.FUTURE_AVAILABILITY]: 'Zukunftssignale', + } + const colors: Partial> = { + [ResultType.VERIFIED_PORTFOLIO]: '#1e3a5f', + [ResultType.EXTERNAL_MARKET]: '#d97706', + [ResultType.FUTURE_AVAILABILITY]: '#7c3aed', + } + const isActive = filterSource === source + return ( + setFilterSource(source)} + sx={{ + bgcolor: isActive ? (colors[source] ?? '#1e3a5f') : 'transparent', + color: isActive ? 'white' : 'text.secondary', + border: `1px solid ${isActive ? (colors[source] ?? '#1e3a5f') : '#e2e8f0'}`, + fontWeight: isActive ? 600 : 400, + }} + /> + ) + })} + + + + Sortierung: + {([['score', 'Relevanz'], ['area', 'Fläche'], ['rent', 'Mietpreis']] as [SortBy, string][]).map(([val, label]) => ( + setSortBy(val)} + sx={{ + bgcolor: sortBy === val ? '#1e3a5f' : 'transparent', + color: sortBy === val ? 'white' : 'text.secondary', + border: `1px solid ${sortBy === val ? '#1e3a5f' : '#e2e8f0'}`, + }} + /> + ))} + + + + + {/* Results */} + {sorted.length === 0 ? ( + + ) : ( + sorted.map(({ match, property }) => ( + + )) + )} + + + {/* Compare Tray */} + {compareTray.length > 0 && ( + + + {compareTray.length} Objekte zum Vergleich ausgewählt + + + + + + + )} + + ) +} diff --git a/src/pages/demand/Shortlists.tsx b/src/pages/demand/Shortlists.tsx new file mode 100644 index 0000000..58c5c79 --- /dev/null +++ b/src/pages/demand/Shortlists.tsx @@ -0,0 +1,174 @@ +import { + Box, + Button, + Card, + Chip, + Typography, + Stack, + Divider, + List, + ListItem, + ListItemText, +} from '@mui/material' +import { Bookmark } from 'lucide-react' + +interface MockShortlist { + id: string + title: string + company: string + assetType: string + objectCount: number + createdAt: string + updatedAt: string + properties: string[] +} + +const MOCK_SHORTLISTS: MockShortlist[] = [ + { + id: 'sl-001', + title: 'Bürosuche Innovatech AG', + company: 'Innovatech AG', + assetType: 'Büro', + objectCount: 2, + createdAt: '01.05.2025', + updatedAt: '08.05.2025', + properties: ['Bürofläche Zollstrasse 12', 'Gemischte Gewerbeeinheit Europaallee'], + }, + { + id: 'sl-002', + title: 'Logistik Basel — Schweizer Logistik', + company: 'Schweizer Logistik GmbH', + assetType: 'Logistik', + objectCount: 1, + createdAt: '03.05.2025', + updatedAt: '03.05.2025', + properties: ['Lagerfläche Hardstrasse 44'], + }, +] + +export default function Shortlists() { + return ( + + {/* Page Header */} + + + + Shortlists + + + Gespeicherte Objektlisten und Entscheidungsvorlagen + + + + + + + + {MOCK_SHORTLISTS.map(sl => ( + + {/* Card header */} + + + + {sl.title} + + + {sl.company} · {sl.assetType} + + + + + + {/* Dates */} + + + Erstellt: {sl.createdAt} + + + Zuletzt aktualisiert: {sl.updatedAt} + + + + + + {/* Property list */} + + {sl.properties.map((prop, i) => ( + + + + + ))} + + + {/* Actions */} + + + + + + ))} + + {/* Empty shortlist prompt */} + + + Objekte aus den Suchergebnissen zur Shortlist hinzufügen + + + + + + ) +} diff --git a/src/pages/ops/AIMonitoring.tsx b/src/pages/ops/AIMonitoring.tsx new file mode 100644 index 0000000..ba73c58 --- /dev/null +++ b/src/pages/ops/AIMonitoring.tsx @@ -0,0 +1,280 @@ +import { + Box, + Card, + Chip, + Typography, + LinearProgress, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Alert, + CircularProgress, +} from '@mui/material' +import { useQuery } from '@tanstack/react-query' +import { propertyService } from '../../services/propertyService' + +interface MetricCard { + label: string + value: string + color: string + note: string +} + +const HEALTH_METRICS: MetricCard[] = [ + { label: 'Extraktionsgenauigkeit', value: '94%', color: '#1a7a4a', note: 'Ø letzte 30 Tage' }, + { label: 'Konfidenz-Ø', value: '73%', color: '#d97706', note: 'Alle Objekte' }, + { label: 'Validierungsrate', value: '88%', color: '#1a7a4a', note: 'Menschliche Bestätigung' }, + { label: 'Fehlerrate', value: '2.1%', color: '#1a7a4a', note: 'Kritische Fehler' }, +] + +interface AIDecision { + timestamp: string + type: string + confidence: string + result: string + impact: string +} + +const RECENT_DECISIONS: AIDecision[] = [ + { + timestamp: '15.05.2025 14:32', + type: 'Bedarfsextraktion', + confidence: '91%', + result: 'Kriterien extrahiert', + impact: 'Suche ausgelöst', + }, + { + timestamp: '15.05.2025 11:15', + type: 'Match-Scoring', + confidence: '88%', + result: '3 Matches berechnet', + impact: 'Review ausgelöst', + }, + { + timestamp: '14.05.2025 16:40', + type: 'Signal-Erkennung', + confidence: '72%', + result: 'Expansion erkannt', + impact: 'Signal erstellt', + }, + { + timestamp: '14.05.2025 09:00', + type: 'Datenqualitätsprüfung', + confidence: '95%', + result: '2 Warnungen erkannt', + impact: 'Meldung erstellt', + }, + { + timestamp: '13.05.2025 15:22', + type: 'Match-Scoring', + confidence: '84%', + result: '2 Matches berechnet', + impact: 'Review ausgelöst', + }, + { + timestamp: '12.05.2025 10:11', + type: 'Signal-Erkennung', + confidence: '65%', + result: 'Möglicher Auszug erkannt', + impact: 'Signal erstellt', + }, +] + +function getConfidenceBadge(pct: string) { + const n = parseInt(pct) + const color = n >= 85 ? '#1a7a4a' : n >= 70 ? '#d97706' : '#c0392b' + return ( + + ) +} + +export default function AIMonitoring() { + const { data: propResp, isLoading } = useQuery({ + queryKey: ['properties'], + queryFn: () => propertyService.getAll(), + }) + + const properties = propResp?.data ?? [] + + const highConf = properties.filter(p => p.confidenceScore > 0.85).length + const midConf = properties.filter(p => p.confidenceScore >= 0.65 && p.confidenceScore <= 0.85).length + const lowConf = properties.filter(p => p.confidenceScore < 0.65).length + const total = properties.length || 1 + + return ( + + {/* Page Header */} + + + + + AI Monitoring + + + + + AI-Layer Gesundheit und Entscheidungsqualität + + + + + + {/* Health Metrics */} + + {HEALTH_METRICS.map(m => ( + + + {m.label} + + + {m.value} + + + {m.note} + + + ))} + + + + {/* Confidence Distribution */} + + + Konfidenzverteilung + + + {isLoading ? ( + + + + ) : ( + + + + Hoch (>85%) + {highConf} Objekte + + + + + + Mittel (65–85%) + {midConf} Objekte + + + + + + Niedrig (<65%) + {lowConf} Objekte + + + + + )} + + + {/* Anomaly Alerts */} + + + Anomalien + + + + Mietpreisangaben für prop-004 weichen von Marktdurchschnitt ab (±31%). Manuelle Prüfung empfohlen. + + + Keine kritischen Anomalien erkannt. System läuft stabil. + + + + + + {/* Recent AI Decisions */} + + + + Letzte KI-Entscheidungen + + + + + + Zeitpunkt + Entscheidungstyp + Konfidenz + Ergebnis + Einfluss + + + + {RECENT_DECISIONS.map((d, i) => ( + + + {d.timestamp} + + + {d.type} + + {getConfidenceBadge(d.confidence)} + + {d.result} + + + {d.impact} + + + ))} + +
+
+
+
+ ) +} diff --git a/src/pages/ops/Governance.tsx b/src/pages/ops/Governance.tsx new file mode 100644 index 0000000..5f15c8f --- /dev/null +++ b/src/pages/ops/Governance.tsx @@ -0,0 +1,311 @@ +import { useState } from 'react' +import { + Box, + Button, + Card, + Chip, + Typography, + Stack, + CircularProgress, +} from '@mui/material' +import { + Building2, + Edit, + CheckCircle, + XCircle, + TrendingUp, + Search, + ClipboardList, +} from 'lucide-react' +import { useQuery } from '@tanstack/react-query' +import { governanceService, type ActivityEventType, type ActivityEvent } from '../../services/governanceService' +import { EmptyState } from '../../components/ui' + +function getEventLabel(type: ActivityEventType): string { + switch (type) { + case 'PROPERTY_CREATED': return 'Objekt erstellt' + case 'PROPERTY_UPDATED': return 'Objekt aktualisiert' + case 'MATCH_APPROVED': return 'Match genehmigt' + case 'MATCH_REJECTED': return 'Match abgelehnt' + case 'SIGNAL_VERIFIED': return 'Signal verifiziert' + case 'NEED_CREATED': return 'Bedarf erstellt' + case 'REVIEW_REQUESTED': return 'Überprüfung angefordert' + } +} + +function getEventDescription(event: ActivityEvent): string { + const actor = event.performedBy + const action = getEventLabel(event.type) + const entity = `${event.entityType} ${event.entityId}` + return `${actor} hat ${entity} — ${action}` +} + +function getEventColor(type: ActivityEventType): string { + switch (type) { + case 'PROPERTY_CREATED': return '#1e3a5f' + case 'PROPERTY_UPDATED': return '#1e3a5f' + case 'MATCH_APPROVED': return '#1a7a4a' + case 'MATCH_REJECTED': return '#c0392b' + case 'SIGNAL_VERIFIED': return '#7c3aed' + case 'NEED_CREATED': return '#0891b2' + case 'REVIEW_REQUESTED': return '#d97706' + } +} + +function getEventIcon(type: ActivityEventType) { + const size = 14 + switch (type) { + case 'PROPERTY_CREATED': return + case 'PROPERTY_UPDATED': return + case 'MATCH_APPROVED': return + case 'MATCH_REJECTED': return + case 'SIGNAL_VERIFIED': return + case 'NEED_CREATED': return + case 'REVIEW_REQUESTED': return + } +} + +const ALL_EVENT_TYPES: ActivityEventType[] = [ + 'PROPERTY_CREATED', + 'PROPERTY_UPDATED', + 'MATCH_APPROVED', + 'MATCH_REJECTED', + 'SIGNAL_VERIFIED', + 'NEED_CREATED', + 'REVIEW_REQUESTED', +] + +function formatDateTime(dateStr: string): string { + const d = new Date(dateStr) + return d.toLocaleDateString('de-CH', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }) + ', ' + d.toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' }) +} + +function isToday(dateStr: string): boolean { + const d = new Date(dateStr) + const now = new Date() + return d.getFullYear() === now.getFullYear() && + d.getMonth() === now.getMonth() && + d.getDate() === now.getDate() +} + +export default function Governance() { + const [filterType, setFilterType] = useState('ALL') + + const { data: activityResp, isLoading, error } = useQuery({ + queryKey: ['activity', 'org-wincasa'], + queryFn: () => governanceService.getActivityLog('org-wincasa'), + }) + + const events = activityResp?.data ?? [] + + const presentTypes = [...new Set(events.map(e => e.type))] + const todayCount = events.filter(e => isToday(e.createdAt)).length + const uniqueUsers = new Set(events.map(e => e.performedBy)).size + + const filtered = filterType === 'ALL' + ? events + : events.filter(e => e.type === filterType) + + if (isLoading) { + return ( + + + + ) + } + + if (error) { + return ( + + Fehler beim Laden des Aktivitätslogs. + + ) + } + + return ( + + {/* Page Header */} + + + + Governance & Aktivitätslog + + + Vollständiger Audit-Trail aller Plattformaktionen + + + + + + + {/* Stats row */} + + + + Ereignisse gesamt + + + {events.length} + + Alle Aktivitäten + + + + Ereignisse heute + + + {todayCount} + + Heutige Aktivitäten + + + + Aktive Benutzer + + + {uniqueUsers} + + Unterschiedliche Nutzer + + + + {/* Filter chips */} + + setFilterType('ALL')} + sx={{ + bgcolor: filterType === 'ALL' ? '#1e3a5f' : 'transparent', + color: filterType === 'ALL' ? 'white' : 'text.secondary', + border: `1px solid ${filterType === 'ALL' ? '#1e3a5f' : '#e2e8f0'}`, + fontWeight: filterType === 'ALL' ? 600 : 400, + }} + /> + {ALL_EVENT_TYPES.filter(t => presentTypes.includes(t)).map(t => ( + setFilterType(t)} + sx={{ + bgcolor: filterType === t ? getEventColor(t) : 'transparent', + color: filterType === t ? 'white' : 'text.secondary', + border: `1px solid ${filterType === t ? getEventColor(t) : '#e2e8f0'}`, + fontWeight: filterType === t ? 600 : 400, + }} + /> + ))} + + + {/* Activity Timeline */} + + + Aktivitätslog + + + {filtered.length === 0 ? ( + + ) : ( + + {/* Vertical line */} + + + + {filtered.map((event, idx) => ( + + {/* Icon dot */} + + {getEventIcon(event.type)} + + + {/* Content */} + + + {getEventDescription(event)} + + {event.notes && ( + + {event.notes} + + )} + + + + + + {/* Timestamp */} + + {formatDateTime(event.createdAt)} + + + ))} + + + )} + + + + ) +} diff --git a/src/pages/ops/ReviewQueue.tsx b/src/pages/ops/ReviewQueue.tsx new file mode 100644 index 0000000..e44ff90 --- /dev/null +++ b/src/pages/ops/ReviewQueue.tsx @@ -0,0 +1,380 @@ +import { useState } from 'react' +import { + Box, + Button, + Card, + Chip, + Typography, + TextField, + Stack, + CircularProgress, + Divider, + Alert, +} from '@mui/material' +import { Target, TrendingUp } from 'lucide-react' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { matchService } from '../../services/matchService' +import { futureSignalService } from '../../services/futureSignalService' +import { RiskLevel } from '../../domain/enums' +import { EmptyState } from '../../components/ui' + +type ReviewItemType = 'MATCH' | 'SIGNAL' + +interface ReviewItem { + id: string + type: ReviewItemType + title: string + confidence: number + risk?: RiskLevel + summary?: string + probability?: number +} + +function getPriorityLabel(confidence: number): string { + return confidence > 0.8 ? 'Kritisch' : 'Normal' +} + +function getPriorityColor(confidence: number): 'error' | 'primary' { + return confidence > 0.8 ? 'error' : 'primary' +} + +function getRiskLabel(risk?: RiskLevel): string { + if (!risk) return '–' + switch (risk) { + case RiskLevel.LOW: return 'Niedrig' + case RiskLevel.MEDIUM: return 'Mittel' + case RiskLevel.HIGH: return 'Hoch' + case RiskLevel.CRITICAL: return 'Kritisch' + } +} + +function getRiskColor(risk?: RiskLevel): 'success' | 'warning' | 'error' | 'default' { + if (!risk) return 'default' + if (risk === RiskLevel.LOW) return 'success' + if (risk === RiskLevel.MEDIUM) return 'warning' + return 'error' +} + +export default function ReviewQueue() { + const queryClient = useQueryClient() + const [activeItem, setActiveItem] = useState(null) + const [reviewNotes, setReviewNotes] = useState('') + const [approvedIds, setApprovedIds] = useState>(new Set()) + const [rejectedIds, setRejectedIds] = useState>(new Set()) + + const { data: matchResp, isLoading: matchLoading } = useQuery({ + queryKey: ['matches'], + queryFn: () => matchService.getAll(), + }) + const { data: signalResp, isLoading: signalLoading } = useQuery({ + queryKey: ['futureSignals'], + queryFn: () => futureSignalService.getAll(), + }) + + const matches = matchResp?.data ?? [] + const signals = signalResp?.data ?? [] + + // Review items = matches NOT approved + future signals NOT verified + const matchItems: ReviewItem[] = matches + .filter(m => !m.isApproved && !approvedIds.has(m.id) && !rejectedIds.has(m.id)) + .map(m => ({ + id: m.id, + type: 'MATCH' as ReviewItemType, + title: `Match: ${m.propertyId} / ${m.needId}`, + confidence: m.confidenceLevel, + risk: m.riskLevel, + summary: m.explainabilitySummary, + })) + + const signalItems: ReviewItem[] = signals + .filter(s => !s.isVerified && !approvedIds.has(s.id) && !rejectedIds.has(s.id)) + .map(s => ({ + id: s.id, + type: 'SIGNAL' as ReviewItemType, + title: `${s.signalType}: ${s.locationHint}`, + confidence: s.confidenceScore, + risk: s.riskLevel, + probability: s.probability, + summary: s.disclaimer, + })) + + const allItems = [...matchItems, ...signalItems] + const selectedItem = allItems.find(i => i.id === activeItem) + + const isLoading = matchLoading || signalLoading + + const handleApprove = async () => { + if (!activeItem) return + const item = allItems.find(i => i.id === activeItem) + if (item?.type === 'MATCH') { + await matchService.approve(activeItem, 'admin@ideal-sharing.ch') + await queryClient.invalidateQueries({ queryKey: ['matches'] }) + } else if (item?.type === 'SIGNAL') { + await futureSignalService.verify(activeItem, 'admin@ideal-sharing.ch') + await queryClient.invalidateQueries({ queryKey: ['futureSignals'] }) + } + setApprovedIds(prev => new Set([...prev, activeItem])) + setActiveItem(null) + setReviewNotes('') + } + + const handleReject = () => { + if (!activeItem) return + setRejectedIds(prev => new Set([...prev, activeItem])) + setActiveItem(null) + setReviewNotes('') + } + + if (isLoading) { + return ( + + + + ) + } + + const totalPending = matchItems.length + signalItems.length + + return ( + + {/* Page Header */} + + + + + Review Queue + + {totalPending > 0 && ( + + )} + + + Human-in-the-loop Prüfung + + + + + + {/* Stats row */} + + + + Offene Reviews + + 0 ? 'warning.main' : 'text.primary' }}> + {totalPending} + + + Ausstehende Prüfungen + + + + + Signale zur Prüfung + + 0 ? 'warning.main' : 'text.primary' }}> + {signalItems.length} + + + Unverifizierte Signale + + + + + {/* Two-column layout */} + + {/* Left: Item List */} + + + + Ausstehende Elemente + + + + {allItems.length === 0 ? ( + + ) : ( + + {allItems.map(item => ( + { + setActiveItem(item.id) + setReviewNotes('') + }} + sx={{ + px: 2, + py: 1.5, + cursor: 'pointer', + borderBottom: '1px solid #f8fafc', + bgcolor: activeItem === item.id ? '#eff6ff' : 'white', + '&:hover': { bgcolor: activeItem === item.id ? '#eff6ff' : '#fafafa' }, + display: 'flex', + alignItems: 'flex-start', + gap: 1.5, + }} + > + + {item.type === 'MATCH' + ? + : + } + + + + {item.title} + + + + + + + + ))} + + )} + + + {/* Right: Review Panel */} + + {!selectedItem ? ( + + ) : ( + + + + + + {selectedItem.title} + + + + + + {/* Key facts */} + + + + Konfidenz + + {Math.round(selectedItem.confidence * 100)}% + + + {selectedItem.probability != null && ( + + Wahrscheinlichkeit + + {Math.round(selectedItem.probability * 100)}% + + + )} + + Risiko + + + + + + {selectedItem.summary && ( + + {selectedItem.summary} + + )} + + + + {/* Notes */} + + Notizen + + setReviewNotes(e.target.value)} + size="small" + sx={{ mb: 2 }} + /> + + {/* Decision buttons */} + + + + + + + + )} + + + + + ) +} diff --git a/src/pages/supply/DataQuality.tsx b/src/pages/supply/DataQuality.tsx new file mode 100644 index 0000000..0771c54 --- /dev/null +++ b/src/pages/supply/DataQuality.tsx @@ -0,0 +1,309 @@ +import { + Alert, + Box, + Card, + Chip, + LinearProgress, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, +} from '@mui/material' +import { useQuery } from '@tanstack/react-query' +import { ErrorState, LoadingPage, SectionContainer } from '../../components/ui' +import { propertyService } from '../../services/propertyService' +import { DataFreshness, ResultType } from '../../domain/enums' + +function getResultTypeLabel(type: ResultType): string { + switch (type) { + case ResultType.VERIFIED_PORTFOLIO: return 'Verified Portfolio' + case ResultType.EXTERNAL_MARKET: return 'Marktinserat' + case ResultType.FUTURE_AVAILABILITY: return 'Zukunftssignal' + } +} + +function getQualityColor(score: number): 'success' | 'warning' | 'error' { + if (score >= 0.8) return 'success' + if (score >= 0.6) return 'warning' + return 'error' +} + +function getFreshnessLabel(freshness: DataFreshness): string { + switch (freshness) { + case DataFreshness.FRESH: return 'Aktuell' + case DataFreshness.STALE: return 'Veraltet' + case DataFreshness.OUTDATED: return 'Abgelaufen' + } +} + +function getFreshnessColor(freshness: DataFreshness): 'success' | 'warning' | 'error' { + switch (freshness) { + case DataFreshness.FRESH: return 'success' + case DataFreshness.STALE: return 'warning' + case DataFreshness.OUTDATED: return 'error' + } +} + +export default function DataQuality() { + const { data: resp, isLoading, error } = useQuery({ + queryKey: ['properties'], + queryFn: () => propertyService.getAll(), + }) + + if (isLoading) return + if (error) return + + const properties = resp?.data ?? [] + + const avgScore = properties.length + ? properties.reduce((sum, p) => sum + p.dataQuality.score, 0) / properties.length + : 0 + + const criticalIssues = properties.filter(p => p.dataQuality.missingCriticalFields.length > 0) + const staleData = properties.filter( + p => p.dataQuality.freshness === DataFreshness.STALE || p.dataQuality.freshness === DataFreshness.OUTDATED + ) + const highQuality = properties.filter(p => p.dataQuality.score >= 0.8) + const medQuality = properties.filter(p => p.dataQuality.score >= 0.6 && p.dataQuality.score < 0.8) + const lowQuality = properties.filter(p => p.dataQuality.score < 0.6) + + // Sort by score ascending (worst first) + const sortedProperties = [...properties].sort((a, b) => a.dataQuality.score - b.dataQuality.score) + + return ( + + {/* Page Header */} + + Datenqualität + Vollständigkeit und Aktualität der Objektdaten + + + {/* Content */} + + + {/* Summary Stats Row */} + + {/* Avg Quality Score */} + + Ø Qualitätsscore + = 0.8 ? '#1a7a4a' : avgScore >= 0.6 ? '#d97706' : '#c0392b' }}> + {Math.round(avgScore * 100)}% + + + + + + + {/* Critical Issues */} + + Kritische Felder fehlen + 2 ? '#c0392b' : criticalIssues.length > 0 ? '#d97706' : '#1a7a4a' }} + > + {criticalIssues.length} + + + von {properties.length} Objekten + + + + {/* Stale Data */} + + Veraltete Daten + 2 ? '#c0392b' : staleData.length > 0 ? '#d97706' : '#1a7a4a' }} + > + {staleData.length} + + + von {properties.length} Objekten + + + + + {/* Quality Distribution */} + + + + {/* High */} + + Hoch (≥80%) + + + 0 ? (highQuality.length / properties.length) * 100 : 0} + color="success" + sx={{ height: 10, borderRadius: 5 }} + /> + + + {properties.length > 0 ? Math.round((highQuality.length / properties.length) * 100) : 0}% + + + + {/* Medium */} + + Mittel (60–79%) + + + 0 ? (medQuality.length / properties.length) * 100 : 0} + color="warning" + sx={{ height: 10, borderRadius: 5 }} + /> + + + {properties.length > 0 ? Math.round((medQuality.length / properties.length) * 100) : 0}% + + + + {/* Low */} + + Niedrig ({'<'}60%) + + + 0 ? (lowQuality.length / properties.length) * 100 : 0} + color="error" + sx={{ height: 10, borderRadius: 5 }} + /> + + + {properties.length > 0 ? Math.round((lowQuality.length / properties.length) * 100) : 0}% + + + + + + + {/* Properties Quality Table */} + + + + + + Objekt + Quelle + Score + Kritische Felder + Optionale Felder + Aktualität + Warnungen + + + + {sortedProperties.map(property => { + const hasCritical = property.dataQuality.missingCriticalFields.length > 0 + const missingCritical = property.dataQuality.missingCriticalFields + const missingOptional = property.dataQuality.missingOptionalFields + const score = property.dataQuality.score + + return ( + + {/* Objekt */} + + {property.title} + + + {/* Quelle */} + + + {getResultTypeLabel(property.resultType)} + + + + {/* Score */} + + + + = 0.8 ? '#1a7a4a' : score >= 0.6 ? '#d97706' : '#c0392b' }}> + {Math.round(score * 100)}% + + + + + + + {/* Kritische Felder */} + + {missingCritical.length === 0 ? ( + + ) : ( + + {missingCritical.slice(0, 2).map(f => ( + + ))} + {missingCritical.length > 2 && ( + + +{missingCritical.length - 2} weitere + + )} + + )} + + + {/* Optionale Felder */} + + {missingOptional.length === 0 ? ( + + ) : ( + + {missingOptional.length} fehlen + + )} + + + {/* Aktualität */} + + + + + {/* Warnungen */} + + {property.dataQuality.warnings.length > 0 ? ( + + {property.dataQuality.warnings[0]} + + ) : ( + + )} + + + ) + })} + +
+
+
+ +
+
+ ) +} diff --git a/src/pages/supply/FutureAvailability.tsx b/src/pages/supply/FutureAvailability.tsx new file mode 100644 index 0000000..7184711 --- /dev/null +++ b/src/pages/supply/FutureAvailability.tsx @@ -0,0 +1,237 @@ +import { + Box, + Button, + Card, + Chip, + Divider, + LinearProgress, + Typography, +} from '@mui/material' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { EmptyState, ErrorState, LoadingPage } from '../../components/ui' +import { futureSignalService } from '../../services/futureSignalService' +import { SignalType } from '../../domain/enums' + +function getSignalTypeLabel(type: SignalType): string { + switch (type) { + case SignalType.EXPANSION: return 'Expansion' + case SignalType.POSSIBLE_MOVE_OUT: return 'Möglicher Auszug' + case SignalType.CONSTRUCTION_PROJECT: return 'Bauprojekt' + case SignalType.RESTRUCTURING: return 'Restrukturierung' + case SignalType.PROJECT_DEVELOPMENT: return 'Projektentwicklung' + case SignalType.SPACE_CONSOLIDATION: return 'Flächenkonsolidierung' + } +} + +function getSignalTypeColor(type: SignalType): string { + switch (type) { + case SignalType.EXPANSION: return '#1a7a4a' + case SignalType.POSSIBLE_MOVE_OUT: return '#d97706' + case SignalType.CONSTRUCTION_PROJECT: return '#1e3a5f' + case SignalType.RESTRUCTURING: return '#ea580c' + case SignalType.PROJECT_DEVELOPMENT: return '#7c3aed' + case SignalType.SPACE_CONSOLIDATION: return '#6b7280' + } +} + +function getProbabilityColor(prob: number): 'success' | 'warning' | 'error' { + if (prob > 0.7) return 'success' + if (prob >= 0.5) return 'warning' + return 'error' +} + +function formatDate(dateStr?: string): string { + if (!dateStr) return '–' + return new Date(dateStr).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' }) +} + +function getSourceTypeLabel(type: string): string { + switch (type) { + case 'PRESS': return 'Pressebericht' + case 'CONSTRUCTION_PERMIT': return 'Baubewilligung' + case 'JOB_POSTING': return 'Stelleninserat' + case 'COMPANY_REPORT': return 'Geschäftsbericht' + case 'MARKET_DATA': return 'Marktdaten' + case 'MANUAL': return 'Manuell' + default: return type + } +} + +export default function FutureAvailability() { + const queryClient = useQueryClient() + + const { data: resp, isLoading, error } = useQuery({ + queryKey: ['futureSignals'], + queryFn: () => futureSignalService.getAll(), + }) + + const verifyMutation = useMutation({ + mutationFn: (signalId: string) => futureSignalService.verify(signalId, 'admin@ideal-sharing.ch'), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['futureSignals'] }), + }) + + if (isLoading) return + if (error) return + + const signals = resp?.data ?? [] + + const totalCount = signals.length + const verifiedCount = signals.filter(s => s.isVerified).length + const highProbCount = signals.filter(s => s.probability > 0.7).length + const avgProbability = signals.length + ? signals.reduce((sum, s) => sum + s.probability, 0) / signals.length + : 0 + + return ( + + {/* Page Header */} + + + + Marktchancen + + + KI-generierte Verfügbarkeitssignale + + + + {/* Content */} + + + {/* Stats Row */} + + + Signale gesamt + {totalCount} + + + Verifiziert + {verifiedCount} + + + Hohe Wahrscheinlichkeit + {highProbCount} + + + Ø Wahrscheinlichkeit + + {Math.round(avgProbability * 100)}% + + + + + {/* Signal Cards Grid */} + {signals.length === 0 ? ( + + ) : ( + + {signals.map(signal => ( + + {/* Card Header */} + + + + + + {/* Location */} + + {signal.locationHint} + {signal.companyName && ( + {signal.companyName} + )} + + + {/* Probability */} + + + Wahrscheinlichkeit + 0.7 ? '#1a7a4a' : signal.probability >= 0.5 ? '#d97706' : '#c0392b' }}> + {Math.round(signal.probability * 100)}% + + + + + + {/* Details */} + + {signal.areaSqmEstimate && ( + + Fläche: ca. {signal.areaSqmEstimate.toLocaleString('de-CH')} m² + + )} + + Zeithorizont: {signal.timeHorizonMonths} Monate + + {signal.expiresAt && ( + + Verfügbar ab: {formatDate(signal.expiresAt)} + + )} + + + {/* Source */} + + + Quelle: {getSourceTypeLabel(signal.source.type)} — Glaubwürdigkeit:{' '} + + {signal.source.credibility === 'HIGH' ? 'Hoch' : signal.source.credibility === 'MEDIUM' ? 'Mittel' : 'Niedrig'} + + + + + {/* Verification Status */} + + {signal.isVerified ? ( + + + {formatDate(signal.verifiedAt)} + + ) : ( + + )} + + + + + {/* Footer buttons */} + + {!signal.isVerified && ( + + )} + + + + ))} + + )} + + + ) +} diff --git a/src/pages/supply/MatchCenter.tsx b/src/pages/supply/MatchCenter.tsx new file mode 100644 index 0000000..3665792 --- /dev/null +++ b/src/pages/supply/MatchCenter.tsx @@ -0,0 +1,283 @@ +import { + Box, + Button, + Card, + Chip, + Divider, + LinearProgress, + Typography, +} from '@mui/material' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { Building2 } from 'lucide-react' +import { useState } from 'react' +import { EmptyState, ErrorState, LoadingPage } from '../../components/ui' +import { matchService } from '../../services/matchService' +import { propertyService } from '../../services/propertyService' +import { needService } from '../../services/needService' +import { MatchStrength, RiskLevel } from '../../domain/enums' + +function getMatchStrengthLabel(strength: MatchStrength): string { + switch (strength) { + case MatchStrength.STRONG: return 'Stark' + case MatchStrength.MODERATE: return 'Mittel' + case MatchStrength.WEAK: return 'Schwach' + } +} + +function getMatchStrengthColor(strength: MatchStrength): 'success' | 'warning' | 'error' { + switch (strength) { + case MatchStrength.STRONG: return 'success' + case MatchStrength.MODERATE: return 'warning' + case MatchStrength.WEAK: return 'error' + } +} + +function getScoreColor(strength: MatchStrength): string { + switch (strength) { + case MatchStrength.STRONG: return '#1a7a4a' + case MatchStrength.MODERATE: return '#d97706' + case MatchStrength.WEAK: return '#c0392b' + } +} + +function getConfidenceColor(score: number): 'success' | 'primary' | 'warning' { + if (score >= 0.85) return 'success' + if (score >= 0.65) return 'primary' + return 'warning' +} + +function getConfidenceLabel(score: number): string { + if (score >= 0.85) return 'Hoch' + if (score >= 0.65) return 'Mittel' + return 'Niedrig' +} + +function getRiskLabel(level: RiskLevel): string { + switch (level) { + case RiskLevel.LOW: return 'Niedriges Risiko' + case RiskLevel.MEDIUM: return 'Mittleres Risiko' + case RiskLevel.HIGH: return 'Hohes Risiko' + case RiskLevel.CRITICAL: return 'Kritisches Risiko' + } +} + +function getRiskColor(level: RiskLevel): 'success' | 'warning' | 'error' { + switch (level) { + case RiskLevel.LOW: return 'success' + case RiskLevel.MEDIUM: return 'warning' + case RiskLevel.HIGH: return 'error' + case RiskLevel.CRITICAL: return 'error' + } +} + +export default function MatchCenter() { + const [filterStrength, setFilterStrength] = useState('ALL') + + const queryClient = useQueryClient() + + const { data: matchResp, isLoading: matchLoading, error: matchError } = useQuery({ + queryKey: ['matches'], + queryFn: () => matchService.getAll(), + }) + const { data: propResp, isLoading: propLoading, error: propError } = useQuery({ + queryKey: ['properties'], + queryFn: () => propertyService.getAll(), + }) + const { data: needResp, isLoading: needLoading, error: needError } = useQuery({ + queryKey: ['needs'], + queryFn: () => needService.getAll(), + }) + + const approveMutation = useMutation({ + mutationFn: (matchId: string) => matchService.approve(matchId, 'admin@ideal-sharing.ch'), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['matches'] }), + }) + + if (matchLoading || propLoading || needLoading) return + if (matchError || propError || needError) return + + const matches = matchResp?.data ?? [] + const properties = propResp?.data ?? [] + const needs = needResp?.data ?? [] + + const filtered = filterStrength === 'ALL' + ? matches + : matches.filter(m => m.matchStrength === filterStrength) + + const sortedFiltered = [...filtered].sort((a, b) => b.matchScore - a.matchScore) + + const strengthFilters: { value: MatchStrength | 'ALL'; label: string }[] = [ + { value: 'ALL', label: 'Alle' }, + { value: MatchStrength.STRONG, label: 'Stark' }, + { value: MatchStrength.MODERATE, label: 'Mittel' }, + { value: MatchStrength.WEAK, label: 'Schwach' }, + ] + + return ( + + {/* Page Header */} + + + + Match Center + + + KI-gestützte Objekt-Bedarfs-Analyse + + + + {/* Content */} + + + {/* Filter Chips */} + + {strengthFilters.map(f => ( + setFilterStrength(f.value)} + color={ + f.value === MatchStrength.STRONG ? 'success' + : f.value === MatchStrength.MODERATE ? 'warning' + : f.value === MatchStrength.WEAK ? 'error' + : 'default' + } + sx={{ cursor: 'pointer', fontWeight: filterStrength === f.value ? 700 : 400 }} + /> + ))} + + + {/* Match Cards */} + {sortedFiltered.length === 0 ? ( + + ) : ( + + {sortedFiltered.map(match => { + const property = properties.find(p => p.id === match.propertyId) + const need = needs.find(n => n.id === match.needId) + + return ( + + + {/* Left column: score */} + + + {match.matchScore} + + / 100 + + + + {/* Center column: details */} + + {/* Property info */} + + + + {property?.title ?? match.propertyId} + + + {need && ( + + {need.companyName} — {need.assetType} + + )} + + + + {/* Positive factors */} + + {match.positiveFactors.slice(0, 3).map((f, i) => ( + + + ✓ {f.criterion}: {Math.round(f.score)}% + + + + + + ))} + + + {/* Negative factors */} + {match.negativeFactors.slice(0, 2).map((f, i) => ( + + ✗ {f.criterion} + + ))} + + {/* Tradeoffs */} + {match.tradeoffs.length > 0 && ( + + {match.tradeoffs.slice(0, 2).map((t, i) => ( + + ⚠ {t.concern} + + ))} + + )} + + + {/* Right column: actions */} + + + + + {match.isApproved ? ( + + ) : ( + + )} + + + + ) + })} + + )} + + + ) +} diff --git a/src/pages/supply/Properties.tsx b/src/pages/supply/Properties.tsx new file mode 100644 index 0000000..1167e5c --- /dev/null +++ b/src/pages/supply/Properties.tsx @@ -0,0 +1,374 @@ +import { + Box, + Card, + Chip, + IconButton, + LinearProgress, + MenuItem, + Select, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TextField, + Tooltip, + Typography, +} from '@mui/material' +import { useQuery } from '@tanstack/react-query' +import { Eye, MoreHorizontal, Plus } from 'lucide-react' +import { useState } from 'react' +import { EmptyState, ErrorState, LoadingPage } from '../../components/ui' +import { propertyService } from '../../services/propertyService' +import { AssetType, AvailabilityStatus, ResultType } from '../../domain/enums' + +function getAssetTypeLabel(type: AssetType): string { + switch (type) { + case AssetType.OFFICE: return 'Büro' + case AssetType.LOGISTICS: return 'Logistik' + case AssetType.RETAIL: return 'Retail' + case AssetType.GASTRO: return 'Gastro' + case AssetType.PRODUCTION: return 'Produktion' + case AssetType.MIXED: return 'Gemischt' + } +} + +function getAssetTypeColor(type: AssetType): string { + switch (type) { + case AssetType.OFFICE: return '#1e3a5f' + case AssetType.LOGISTICS: return '#d97706' + case AssetType.RETAIL: return '#7c3aed' + case AssetType.GASTRO: return '#0d9488' + case AssetType.PRODUCTION: return '#92400e' + case AssetType.MIXED: return '#6b7280' + } +} + +function getAvailabilityLabel(status: AvailabilityStatus): string { + switch (status) { + case AvailabilityStatus.AVAILABLE_NOW: return 'Verfügbar' + case AvailabilityStatus.AVAILABLE_SOON: return 'Bald verfügbar' + case AvailabilityStatus.FUTURE_SIGNAL: return 'Zukunftssignal' + case AvailabilityStatus.OCCUPIED: return 'Belegt' + case AvailabilityStatus.UNKNOWN: return 'Unbekannt' + } +} + +function getAvailabilityColor(status: AvailabilityStatus): 'success' | 'warning' | 'secondary' | 'error' | 'default' { + switch (status) { + case AvailabilityStatus.AVAILABLE_NOW: return 'success' + case AvailabilityStatus.AVAILABLE_SOON: return 'warning' + case AvailabilityStatus.FUTURE_SIGNAL: return 'secondary' + case AvailabilityStatus.OCCUPIED: return 'error' + case AvailabilityStatus.UNKNOWN: return 'default' + } +} + +function getResultTypeLabel(type: ResultType): string { + switch (type) { + case ResultType.VERIFIED_PORTFOLIO: return 'Verified Portfolio' + case ResultType.EXTERNAL_MARKET: return 'Marktinserat' + case ResultType.FUTURE_AVAILABILITY: return 'Zukunftssignal' + } +} + +function getResultTypeColor(type: ResultType): string { + switch (type) { + case ResultType.VERIFIED_PORTFOLIO: return '#1e3a5f' + case ResultType.EXTERNAL_MARKET: return '#d97706' + case ResultType.FUTURE_AVAILABILITY: return '#7c3aed' + } +} + +function getConfidenceColor(score: number): string { + if (score >= 0.85) return '#1a7a4a' + if (score >= 0.65) return '#1e3a5f' + return '#d97706' +} + +function getQualityColor(score: number): 'success' | 'warning' | 'error' { + if (score >= 0.8) return 'success' + if (score >= 0.6) return 'warning' + return 'error' +} + +export default function Properties() { + const [selectedResultType, setSelectedResultType] = useState('ALL') + const [selectedAssetType, setSelectedAssetType] = useState('ALL') + const [searchQuery, setSearchQuery] = useState('') + + const { data: resp, isLoading, error } = useQuery({ + queryKey: ['properties'], + queryFn: () => propertyService.getAll(), + }) + + if (isLoading) return + if (error) return + + const properties = resp?.data ?? [] + + const filtered = properties.filter(p => { + if (selectedResultType !== 'ALL' && p.resultType !== selectedResultType) return false + if (selectedAssetType !== 'ALL' && p.assetType !== selectedAssetType) return false + if (searchQuery) { + const q = searchQuery.toLowerCase() + const matchesTitle = p.title.toLowerCase().includes(q) + const matchesCity = p.location.city.toLowerCase().includes(q) + const matchesStreet = p.address.street.toLowerCase().includes(q) + if (!matchesTitle && !matchesCity && !matchesStreet) return false + } + return true + }) + + const sourceTypeFilters: { value: ResultType | 'ALL'; label: string; color: string }[] = [ + { value: 'ALL', label: 'Alle', color: '#6b7280' }, + { value: ResultType.VERIFIED_PORTFOLIO, label: 'Verified Portfolio', color: '#1e3a5f' }, + { value: ResultType.EXTERNAL_MARKET, label: 'Marktinserate', color: '#d97706' }, + { value: ResultType.FUTURE_AVAILABILITY, label: 'Zukunftssignale', color: '#7c3aed' }, + ] + + return ( + + {/* Page Header */} + + + Objekte + + + + + + + + + + {/* Content */} + + + {/* Filter Bar */} + + + {/* Row 1: Source type chips */} + + {sourceTypeFilters.map(f => ( + setSelectedResultType(f.value)} + sx={ + selectedResultType === f.value + ? { bgcolor: f.color, color: 'white', borderColor: f.color, fontWeight: 600, cursor: 'pointer' } + : { borderColor: f.color, color: f.color, cursor: 'pointer' } + } + /> + ))} + + {/* Row 2: Asset type select + search */} + + + setSearchQuery(e.target.value)} + placeholder="Suche nach Titel, Stadt, Strasse…" + size="small" + sx={{ ml: 'auto', minWidth: 260 }} + /> + + + + + {/* Properties Table */} + {filtered.length === 0 ? ( + + ) : ( + + + + + Objekt + Typ + Standort + Fläche + Miete/m² + Quelle + Konfidenz + Datenqualität + Verfügbarkeit + Aktionen + + + + {filtered.map(property => { + const hasCritical = property.dataQuality.missingCriticalFields.length > 0 + return ( + + {/* Objekt */} + + {property.title} + + {property.address.street} {property.address.houseNumber}, {property.address.city} + + + + {/* Typ */} + + + + + {/* Standort */} + + {property.location.city} + {property.location.canton && ( + {property.location.canton} + )} + + + {/* Fläche */} + + {property.areaSqm.toLocaleString('de-CH')} m² + + + {/* Miete/m² */} + + CHF {property.rentPricePerSqm} + + + {/* Quelle */} + + + + + {/* Konfidenz */} + + + {Math.round(property.confidenceScore * 100)}% + + + + {/* Datenqualität */} + + + {property.dataQuality.missingCriticalFields.length > 0 && ( + + Kritische Felder fehlen: + {property.dataQuality.missingCriticalFields.map(f => ( + • {f} + ))} + + )} + {property.dataQuality.warnings.length > 0 && ( + + Warnungen: + {property.dataQuality.warnings.map((w, i) => ( + • {w} + ))} + + )} + {property.dataQuality.missingCriticalFields.length === 0 && property.dataQuality.warnings.length === 0 && ( + Keine Probleme + )} + + } + > + + + + {Math.round(property.dataQuality.score * 100)}% + + + + + + {/* Verfügbarkeit */} + + + + + {/* Aktionen */} + + + + + + + + + + + + + + + + + + + + ) + })} + +
+
+ )} +
+
+ ) +} diff --git a/src/pages/supply/SupplyDashboard.tsx b/src/pages/supply/SupplyDashboard.tsx new file mode 100644 index 0000000..1293ac9 --- /dev/null +++ b/src/pages/supply/SupplyDashboard.tsx @@ -0,0 +1,283 @@ +import { Box, Card, Chip, LinearProgress, Table, TableBody, TableCell, TableHead, TableRow, Tooltip, Typography } from '@mui/material' +import { useQuery } from '@tanstack/react-query' +import { AlertTriangle, BarChart2, Building2, Target } from 'lucide-react' +import { SectionContainer, LoadingPage, ErrorState } from '../../components/ui' +import { propertyService } from '../../services/propertyService' +import { matchService } from '../../services/matchService' +import { governanceService } from '../../services/governanceService' +import { ResultType, MatchStrength } from '../../domain/enums' +import type { ActivityEventType } from '../../services/governanceService' + +function getMatchStrengthColor(strength: MatchStrength): 'success' | 'warning' | 'error' { + if (strength === MatchStrength.STRONG) return 'success' + if (strength === MatchStrength.MODERATE) return 'warning' + return 'error' +} + +function getMatchStrengthLabel(strength: MatchStrength): string { + if (strength === MatchStrength.STRONG) return 'Stark' + if (strength === MatchStrength.MODERATE) return 'Mittel' + return 'Schwach' +} + +function getEventDescription(type: ActivityEventType): string { + switch (type) { + case 'PROPERTY_CREATED': return 'hat ein Objekt erstellt' + case 'PROPERTY_UPDATED': return 'hat ein Objekt aktualisiert' + case 'MATCH_APPROVED': return 'hat einen Match genehmigt' + case 'MATCH_REJECTED': return 'hat einen Match abgelehnt' + case 'SIGNAL_VERIFIED': return 'hat ein Signal verifiziert' + case 'NEED_CREATED': return 'hat einen Bedarf erstellt' + case 'REVIEW_REQUESTED': return 'hat eine Überprüfung angefordert' + } +} + +function getEventColor(type: ActivityEventType): string { + switch (type) { + case 'PROPERTY_CREATED': return '#1e3a5f' + case 'PROPERTY_UPDATED': return '#1e3a5f' + case 'MATCH_APPROVED': return '#1a7a4a' + case 'MATCH_REJECTED': return '#c0392b' + case 'SIGNAL_VERIFIED': return '#7c3aed' + case 'NEED_CREATED': return '#d97706' + case 'REVIEW_REQUESTED': return '#d97706' + } +} + +function formatTimeAgo(dateStr: string): string { + const diff = Date.now() - new Date(dateStr).getTime() + const hours = Math.floor(diff / 3600000) + const days = Math.floor(hours / 24) + if (hours < 1) return 'vor weniger als 1 Stunde' + if (hours < 24) return `vor ${hours} Stunde${hours > 1 ? 'n' : ''}` + return `vor ${days} Tag${days > 1 ? 'en' : ''}` +} + +export default function SupplyDashboard() { + const { data: propResp, isLoading: propLoading, error: propError } = useQuery({ + queryKey: ['properties'], + queryFn: () => propertyService.getAll(), + }) + const { data: matchResp, isLoading: matchLoading, error: matchError } = useQuery({ + queryKey: ['matches'], + queryFn: () => matchService.getAll(), + }) + const { data: activityResp, isLoading: activityLoading, error: activityError } = useQuery({ + queryKey: ['activity', 'org-wincasa'], + queryFn: () => governanceService.getActivityLog('org-wincasa'), + }) + + if (propLoading || matchLoading || activityLoading) return + if (propError || matchError || activityError) return + + const properties = propResp?.data ?? [] + const matches = matchResp?.data ?? [] + const activities = activityResp?.data ?? [] + + const avgQuality = properties.length + ? properties.reduce((sum, p) => sum + p.dataQuality.score, 0) / properties.length + : 0 + const pendingReview = properties.filter(p => p.dataQuality.missingCriticalFields.length > 0).length + + const avgQualityColor = avgQuality >= 0.8 ? 'success.main' : avgQuality >= 0.6 ? 'warning.main' : 'error.main' + + const verifiedCount = properties.filter(p => p.resultType === ResultType.VERIFIED_PORTFOLIO).length + const marketCount = properties.filter(p => p.resultType === ResultType.EXTERNAL_MARKET).length + const futureCount = properties.filter(p => p.resultType === ResultType.FUTURE_AVAILABILITY).length + + const topMatches = [...matches].sort((a, b) => b.matchScore - a.matchScore).slice(0, 3) + + return ( + + {/* Page Header */} + + Supply Dashboard + Portfolioübersicht und aktuelle Kennzahlen + + + {/* Content */} + + + {/* Section 1 - KPI Cards */} + + {/* Objekte */} + + + + Objekte + + + {properties.length} + Gesamtportfolio + + + + {/* Aktive Matches */} + + + + Aktive Matches + + + {matches.length} + KI-generierte Matches + + + + {/* Ø Datenqualität */} + + + + Ø Datenqualität + = 0.8 ? '#1a7a4a' : avgQuality >= 0.6 ? '#d97706' : '#c0392b'} /> + + + {Math.round(avgQuality * 100)}% + + Durchschnittlicher Score + + + + {/* Prüfungen ausstehend */} + + + + Prüfungen ausstehend + + + 0 ? 'warning.main' : 'text.primary' }}> + {pendingReview} + + Kritische Felder fehlen + + + + + {/* Section 2 - Portfolio Overview */} + + + {verifiedCount} + Verified Portfolio + + + {marketCount} + Marktinserate + + + {futureCount} + Zukunftssignale + + + + {/* Section 3 - Recent Matches */} + + + + + + Objekt + Unternehmen + Score + Stärke + Aktion + + + + {topMatches.map(match => { + const property = properties.find(p => p.id === match.propertyId) + return ( + + + {property?.title ?? match.propertyId} + + + {match.needId} + + + + {match.matchScore} + + + + + + + + + + + + + + + + + ) + })} + +
+
+
+ + {/* Section 4 - Activity Log */} + + + + {activities.slice(0, 5).map(event => ( + + + + + + + {event.performedBy} {getEventDescription(event.type)} + + {event.notes && ( + {event.notes} + )} + + + {formatTimeAgo(event.createdAt)} + + + ))} + + + + +
+
+ ) +} diff --git a/src/provider/IFutureSignalProvider.ts b/src/provider/IFutureSignalProvider.ts new file mode 100644 index 0000000..81f93f5 --- /dev/null +++ b/src/provider/IFutureSignalProvider.ts @@ -0,0 +1,16 @@ +import type { FutureSignal } from '../domain/futureSignal' +import type { SignalType } from '../domain/enums' + +export interface FutureSignalFilters { + signalType?: SignalType + minProbability?: number + organizationId?: string + isVerified?: boolean +} + +export interface IFutureSignalProvider { + getAll(filters?: FutureSignalFilters): Promise + getById(id: string): Promise + getByProperty(propertyId: string): Promise + verify(id: string, verifiedBy: string): Promise +} diff --git a/src/provider/IMatchProvider.ts b/src/provider/IMatchProvider.ts new file mode 100644 index 0000000..928335e --- /dev/null +++ b/src/provider/IMatchProvider.ts @@ -0,0 +1,18 @@ +import type { Match } from '../domain/match' +import type { MatchStrength } from '../domain/enums' + +export interface MatchFilters { + needId?: string + propertyId?: string + minScore?: number + matchStrength?: MatchStrength + organizationId?: string +} + +export interface IMatchProvider { + getAll(filters?: MatchFilters): Promise + getById(id: string): Promise + getByNeed(needId: string): Promise + getByProperty(propertyId: string): Promise + approve(id: string, reviewedBy: string): Promise +} diff --git a/src/provider/INeedProvider.ts b/src/provider/INeedProvider.ts new file mode 100644 index 0000000..08a2168 --- /dev/null +++ b/src/provider/INeedProvider.ts @@ -0,0 +1,16 @@ +import type { Need, CreateNeedInput, UpdateNeedInput } from '../domain/need' +import type { AssetType } from '../domain/enums' + +export interface NeedFilters { + assetType?: AssetType + organizationId?: string + companyName?: string +} + +export interface INeedProvider { + getAll(filters?: NeedFilters): Promise + getById(id: string): Promise + create(data: CreateNeedInput): Promise + update(id: string, data: UpdateNeedInput): Promise + remove(id: string): Promise +} diff --git a/src/provider/IPropertyProvider.ts b/src/provider/IPropertyProvider.ts new file mode 100644 index 0000000..4401a36 --- /dev/null +++ b/src/provider/IPropertyProvider.ts @@ -0,0 +1,19 @@ +import type { Property, CreatePropertyInput, UpdatePropertyInput } from '../domain/property' +import type { AssetType, ResultType } from '../domain/enums' + +export interface PropertyFilters { + assetType?: AssetType + resultType?: ResultType + city?: string + minAreaSqm?: number + maxRentPerSqm?: number + organizationId?: string +} + +export interface IPropertyProvider { + getAll(filters?: PropertyFilters): Promise + getById(id: string): Promise + create(data: CreatePropertyInput): Promise + update(id: string, data: UpdatePropertyInput): Promise + remove(id: string): Promise +} diff --git a/src/provider/MockupFutureSignalProvider.ts b/src/provider/MockupFutureSignalProvider.ts new file mode 100644 index 0000000..5755e28 --- /dev/null +++ b/src/provider/MockupFutureSignalProvider.ts @@ -0,0 +1,27 @@ +import type { IFutureSignalProvider, FutureSignalFilters } from './IFutureSignalProvider' +import type { FutureSignal } from '../domain/futureSignal' +import { mockFutureSignals } from '../mock-data/futureSignals' + +const store: FutureSignal[] = [...mockFutureSignals] + +export const MockupFutureSignalProvider: IFutureSignalProvider = { + async getAll(filters?: FutureSignalFilters) { + let results = [...store] + if (filters?.signalType) results = results.filter(s => s.signalType === filters.signalType) + if (filters?.minProbability !== undefined) results = results.filter(s => s.probability >= filters.minProbability!) + if (filters?.organizationId) results = results.filter(s => s.organizationId === filters.organizationId) + if (filters?.isVerified !== undefined) results = results.filter(s => s.isVerified === filters.isVerified) + return results.sort((a, b) => b.probability - a.probability) + }, + async getById(id) { + return store.find(s => s.id === id) ?? null + }, + async getByProperty(propertyId) { + return store.filter(s => s.propertyId === propertyId) + }, + async verify(id, verifiedBy) { + const idx = store.findIndex(s => s.id === id) + store[idx] = { ...store[idx], isVerified: true, verifiedBy, verifiedAt: new Date().toISOString(), updatedAt: new Date().toISOString() } + return store[idx] + }, +} diff --git a/src/provider/MockupMatchProvider.ts b/src/provider/MockupMatchProvider.ts new file mode 100644 index 0000000..58e61e6 --- /dev/null +++ b/src/provider/MockupMatchProvider.ts @@ -0,0 +1,31 @@ +import type { IMatchProvider, MatchFilters } from './IMatchProvider' +import type { Match } from '../domain/match' +import { mockMatches } from '../mock-data/matches' + +const store: Match[] = [...mockMatches] + +export const MockupMatchProvider: IMatchProvider = { + async getAll(filters?: MatchFilters) { + let results = [...store] + if (filters?.needId) results = results.filter(m => m.needId === filters.needId) + if (filters?.propertyId) results = results.filter(m => m.propertyId === filters.propertyId) + if (filters?.minScore) results = results.filter(m => m.matchScore >= filters.minScore!) + if (filters?.matchStrength) results = results.filter(m => m.matchStrength === filters.matchStrength) + if (filters?.organizationId) results = results.filter(m => m.organizationId === filters.organizationId) + return results.sort((a, b) => b.matchScore - a.matchScore) + }, + async getById(id) { + return store.find(m => m.id === id) ?? null + }, + async getByNeed(needId) { + return store.filter(m => m.needId === needId).sort((a, b) => b.matchScore - a.matchScore) + }, + async getByProperty(propertyId) { + return store.filter(m => m.propertyId === propertyId).sort((a, b) => b.matchScore - a.matchScore) + }, + async approve(id, reviewedBy) { + const idx = store.findIndex(m => m.id === id) + store[idx] = { ...store[idx], isApproved: true, reviewedBy, reviewedAt: new Date().toISOString(), updatedAt: new Date().toISOString() } + return store[idx] + }, +} diff --git a/src/provider/MockupNeedProvider.ts b/src/provider/MockupNeedProvider.ts new file mode 100644 index 0000000..5df3871 --- /dev/null +++ b/src/provider/MockupNeedProvider.ts @@ -0,0 +1,32 @@ +import type { INeedProvider, NeedFilters } from './INeedProvider' +import type { Need, CreateNeedInput, UpdateNeedInput } from '../domain/need' +import { mockNeeds } from '../mock-data/needs' + +const store: Need[] = [...mockNeeds] + +export const MockupNeedProvider: INeedProvider = { + async getAll(filters?: NeedFilters) { + let results = [...store] + if (filters?.assetType) results = results.filter(n => n.assetType === filters.assetType) + if (filters?.organizationId) results = results.filter(n => n.organizationId === filters.organizationId) + if (filters?.companyName) results = results.filter(n => n.companyName.toLowerCase().includes(filters.companyName!.toLowerCase())) + return results + }, + async getById(id) { + return store.find(n => n.id === id) ?? null + }, + async create(data: CreateNeedInput) { + const next: Need = { id: crypto.randomUUID(), ...data, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } + store.push(next) + return next + }, + async update(id, data: UpdateNeedInput) { + const idx = store.findIndex(n => n.id === id) + store[idx] = { ...store[idx], ...data, updatedAt: new Date().toISOString() } + return store[idx] + }, + async remove(id) { + const idx = store.findIndex(n => n.id === id) + store.splice(idx, 1) + }, +} diff --git a/src/provider/MockupPropertyProvider.ts b/src/provider/MockupPropertyProvider.ts new file mode 100644 index 0000000..6d6d434 --- /dev/null +++ b/src/provider/MockupPropertyProvider.ts @@ -0,0 +1,35 @@ +import type { IPropertyProvider, PropertyFilters } from './IPropertyProvider' +import type { Property, CreatePropertyInput, UpdatePropertyInput } from '../domain/property' +import { mockProperties } from '../mock-data/properties' + +const store: Property[] = [...mockProperties] + +export const MockupPropertyProvider: IPropertyProvider = { + async getAll(filters?: PropertyFilters) { + let results = [...store] + if (filters?.assetType) results = results.filter(p => p.assetType === filters.assetType) + if (filters?.resultType) results = results.filter(p => p.resultType === filters.resultType) + if (filters?.city) results = results.filter(p => p.location.city.toLowerCase().includes(filters.city!.toLowerCase())) + if (filters?.minAreaSqm) results = results.filter(p => p.areaSqm >= filters.minAreaSqm!) + if (filters?.maxRentPerSqm) results = results.filter(p => p.rentPricePerSqm <= filters.maxRentPerSqm!) + if (filters?.organizationId) results = results.filter(p => p.organizationId === filters.organizationId) + return results + }, + async getById(id) { + return store.find(p => p.id === id) ?? null + }, + async create(data: CreatePropertyInput) { + const next: Property = { id: crypto.randomUUID(), ...data, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } + store.push(next) + return next + }, + async update(id, data: UpdatePropertyInput) { + const idx = store.findIndex(p => p.id === id) + store[idx] = { ...store[idx], ...data, updatedAt: new Date().toISOString() } + return store[idx] + }, + async remove(id) { + const idx = store.findIndex(p => p.id === id) + store.splice(idx, 1) + }, +} diff --git a/src/services/aiService.ts b/src/services/aiService.ts new file mode 100644 index 0000000..098aad3 --- /dev/null +++ b/src/services/aiService.ts @@ -0,0 +1,57 @@ +import type { ItemResponse } from './types' +import type { CreateNeedInput } from '../domain/need' + +export interface CriteriaExtractionResult { + extractedCriteria: Partial + confidence: number + missingFields: string[] + assumptions: string[] + followUpQuestions: string[] +} + +export interface AIServiceProvider { + extractCriteria(naturalLanguageInput: string): Promise + generateFollowUp(partialNeed: Partial): Promise +} + +const MockupAIServiceProvider: AIServiceProvider = { + async extractCriteria(input: string): Promise { + // Deterministic mock extraction — simulates AI parsing + return { + extractedCriteria: { + companyName: 'Unbekannt (bitte bestätigen)', + requiredArea: { min: 400, max: 900 }, + budgetRange: { maxPerSqm: 40, currency: 'CHF' }, + }, + confidence: 0.72, + missingFields: ['assetType', 'timing', 'preferredLocations'], + assumptions: ['Fläche aus Zahlenangabe geschätzt', 'Budget aus Kostennennung abgeleitet'], + followUpQuestions: [ + 'Welchen Nutzungstyp suchen Sie (Büro, Retail, Logistik)?', + 'In welchen Städten oder Regionen suchen Sie?', + 'Wann möchten Sie spätestens einziehen?', + ], + } + }, + async generateFollowUp(partialNeed: Partial): Promise { + const questions: string[] = [] + if (!partialNeed.assetType) questions.push('Welchen Nutzungstyp suchen Sie?') + if (!partialNeed.preferredLocations?.length) questions.push('In welchen Regionen suchen Sie?') + if (!partialNeed.timing) questions.push('Was ist Ihr gewünschter Einzugstermin?') + if (!partialNeed.budgetRange) questions.push('Was ist Ihr maximales monatliches Budget?') + return questions + }, +} + +const provider = MockupAIServiceProvider + +export const aiService = { + async extractCriteria(input: string): Promise> { + const data = await provider.extractCriteria(input) + return { data } + }, + async generateFollowUp(partialNeed: Partial): Promise> { + const data = await provider.generateFollowUp(partialNeed) + return { data } + }, +} diff --git a/src/services/futureSignalService.ts b/src/services/futureSignalService.ts new file mode 100644 index 0000000..40d6afb --- /dev/null +++ b/src/services/futureSignalService.ts @@ -0,0 +1,25 @@ +import { MockupFutureSignalProvider } from '../provider/MockupFutureSignalProvider' +import type { FutureSignalFilters } from '../provider/IFutureSignalProvider' +import type { FutureSignal } from '../domain/futureSignal' +import type { ListResponse, ItemResponse } from './types' + +const provider = MockupFutureSignalProvider + +export const futureSignalService = { + async getAll(filters?: FutureSignalFilters): Promise> { + const data = await provider.getAll(filters) + return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } + }, + async getById(id: string): Promise> { + const data = await provider.getById(id) + return { data } + }, + async getByProperty(propertyId: string): Promise> { + const data = await provider.getByProperty(propertyId) + return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } + }, + async verify(id: string, verifiedBy: string): Promise> { + const data = await provider.verify(id, verifiedBy) + return { data } + }, +} diff --git a/src/services/governanceService.ts b/src/services/governanceService.ts new file mode 100644 index 0000000..bc39cf1 --- /dev/null +++ b/src/services/governanceService.ts @@ -0,0 +1,41 @@ +import type { ListResponse, ItemResponse } from './types' + +export type ActivityEventType = + | 'PROPERTY_CREATED' + | 'PROPERTY_UPDATED' + | 'MATCH_APPROVED' + | 'MATCH_REJECTED' + | 'SIGNAL_VERIFIED' + | 'NEED_CREATED' + | 'REVIEW_REQUESTED' + +export interface ActivityEvent { + id: string + type: ActivityEventType + entityId: string + entityType: 'PROPERTY' | 'MATCH' | 'NEED' | 'SIGNAL' + performedBy: string + organizationId: string + notes?: string + createdAt: string +} + +const mockActivityLog: ActivityEvent[] = [ + { id: 'evt-001', type: 'PROPERTY_CREATED', entityId: 'prop-001', entityType: 'PROPERTY', performedBy: 'admin@ideal-sharing.ch', organizationId: 'org-wincasa', createdAt: '2025-01-10T08:00:00Z' }, + { id: 'evt-002', type: 'MATCH_APPROVED', entityId: 'match-003', entityType: 'MATCH', performedBy: 'admin@ideal-sharing.ch', organizationId: 'org-wincasa', notes: 'Starker Match bestätigt', createdAt: '2025-05-10T09:00:00Z' }, + { id: 'evt-003', type: 'SIGNAL_VERIFIED', entityId: 'signal-003', entityType: 'SIGNAL', performedBy: 'admin@ideal-sharing.ch', organizationId: 'org-wincasa', createdAt: '2025-05-05T09:00:00Z' }, +] + +const store = [...mockActivityLog] + +export const governanceService = { + async getActivityLog(organizationId?: string): Promise> { + const data = organizationId ? store.filter(e => e.organizationId === organizationId) : [...store] + return { data: data.sort((a, b) => b.createdAt.localeCompare(a.createdAt)), meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } + }, + async logEvent(event: Omit): Promise> { + const data: ActivityEvent = { id: crypto.randomUUID(), ...event, createdAt: new Date().toISOString() } + store.push(data) + return { data } + }, +} diff --git a/src/services/matchService.ts b/src/services/matchService.ts new file mode 100644 index 0000000..08427e1 --- /dev/null +++ b/src/services/matchService.ts @@ -0,0 +1,29 @@ +import { MockupMatchProvider } from '../provider/MockupMatchProvider' +import type { MatchFilters } from '../provider/IMatchProvider' +import type { Match } from '../domain/match' +import type { ListResponse, ItemResponse } from './types' + +const provider = MockupMatchProvider + +export const matchService = { + async getAll(filters?: MatchFilters): Promise> { + const data = await provider.getAll(filters) + return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } + }, + async getById(id: string): Promise> { + const data = await provider.getById(id) + return { data } + }, + async getByNeed(needId: string): Promise> { + const data = await provider.getByNeed(needId) + return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } + }, + async getByProperty(propertyId: string): Promise> { + const data = await provider.getByProperty(propertyId) + return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } + }, + async approve(id: string, reviewedBy: string): Promise> { + const data = await provider.approve(id, reviewedBy) + return { data } + }, +} diff --git a/src/services/needService.ts b/src/services/needService.ts new file mode 100644 index 0000000..6fb0a08 --- /dev/null +++ b/src/services/needService.ts @@ -0,0 +1,29 @@ +import { MockupNeedProvider } from '../provider/MockupNeedProvider' +import type { NeedFilters } from '../provider/INeedProvider' +import type { Need, CreateNeedInput, UpdateNeedInput } from '../domain/need' +import type { ListResponse, ItemResponse } from './types' + +const provider = MockupNeedProvider + +export const needService = { + async getAll(filters?: NeedFilters): Promise> { + const data = await provider.getAll(filters) + return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } + }, + async getById(id: string): Promise> { + const data = await provider.getById(id) + return { data } + }, + async create(input: CreateNeedInput): Promise> { + const data = await provider.create(input) + return { data } + }, + async update(id: string, input: UpdateNeedInput): Promise> { + const data = await provider.update(id, input) + return { data } + }, + async remove(id: string): Promise> { + await provider.remove(id) + return { data: undefined } + }, +} diff --git a/src/services/propertyService.ts b/src/services/propertyService.ts new file mode 100644 index 0000000..9a542b6 --- /dev/null +++ b/src/services/propertyService.ts @@ -0,0 +1,30 @@ +import { MockupPropertyProvider } from '../provider/MockupPropertyProvider' +import type { PropertyFilters } from '../provider/IPropertyProvider' +import type { CreatePropertyInput, UpdatePropertyInput } from '../domain/property' +import type { ListResponse, ItemResponse } from './types' +import type { Property } from '../domain/property' + +const provider = MockupPropertyProvider + +export const propertyService = { + async getAll(filters?: PropertyFilters): Promise> { + const data = await provider.getAll(filters) + return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } + }, + async getById(id: string): Promise> { + const data = await provider.getById(id) + return { data } + }, + async create(input: CreatePropertyInput): Promise> { + const data = await provider.create(input) + return { data } + }, + async update(id: string, input: UpdatePropertyInput): Promise> { + const data = await provider.update(id, input) + return { data } + }, + async remove(id: string): Promise> { + await provider.remove(id) + return { data: undefined } + }, +} diff --git a/src/services/types.ts b/src/services/types.ts new file mode 100644 index 0000000..62a2ea8 --- /dev/null +++ b/src/services/types.ts @@ -0,0 +1,15 @@ +export interface ServiceMeta { + total: number + page: number + pageSize: number + hasMore: boolean +} + +export interface ServiceResponse { + data: T + meta?: ServiceMeta + error?: string | null +} + +export type ListResponse = ServiceResponse +export type ItemResponse = ServiceResponse diff --git a/src/stores/compareStore.ts b/src/stores/compareStore.ts new file mode 100644 index 0000000..8a5220c --- /dev/null +++ b/src/stores/compareStore.ts @@ -0,0 +1,27 @@ +import { create } from 'zustand' + +const MAX_COMPARE_ITEMS = 3 + +interface CompareState { + compareTray: string[] + addToCompare: (propertyId: string) => void + removeFromCompare: (propertyId: string) => void + clearCompare: () => void + isInCompare: (propertyId: string) => boolean + isFull: () => boolean +} + +export const useCompareStore = create((set, get) => ({ + compareTray: [], + addToCompare: (propertyId) => + set((state) => { + if (state.compareTray.length >= MAX_COMPARE_ITEMS) return state + if (state.compareTray.includes(propertyId)) return state + return { compareTray: [...state.compareTray, propertyId] } + }), + removeFromCompare: (propertyId) => + set((state) => ({ compareTray: state.compareTray.filter(id => id !== propertyId) })), + clearCompare: () => set({ compareTray: [] }), + isInCompare: (propertyId) => get().compareTray.includes(propertyId), + isFull: () => get().compareTray.length >= MAX_COMPARE_ITEMS, +})) diff --git a/src/stores/layoutStore.ts b/src/stores/layoutStore.ts new file mode 100644 index 0000000..d4ebc41 --- /dev/null +++ b/src/stores/layoutStore.ts @@ -0,0 +1,22 @@ +import { create } from 'zustand' +import { WorkspaceType } from '../domain/enums' + +interface LayoutState { + activeWorkspace: WorkspaceType + sidebarCollapsed: boolean + pinnedPanels: string[] + setActiveWorkspace: (workspace: WorkspaceType) => void + toggleSidebar: () => void + pinPanel: (panelId: string) => void + unpinPanel: (panelId: string) => void +} + +export const useLayoutStore = create((set) => ({ + activeWorkspace: WorkspaceType.SUPPLY, + sidebarCollapsed: false, + pinnedPanels: [], + setActiveWorkspace: (workspace) => set({ activeWorkspace: workspace }), + toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })), + pinPanel: (panelId) => set((state) => ({ pinnedPanels: [...state.pinnedPanels, panelId] })), + unpinPanel: (panelId) => set((state) => ({ pinnedPanels: state.pinnedPanels.filter(id => id !== panelId) })), +})) diff --git a/src/stores/sessionStore.ts b/src/stores/sessionStore.ts new file mode 100644 index 0000000..7d93d93 --- /dev/null +++ b/src/stores/sessionStore.ts @@ -0,0 +1,38 @@ +import { create } from 'zustand' +import { UserRole } from '../domain/enums' + +export interface MockUser { + id: string + email: string + name: string + role: UserRole + organizationId: string + organizationName: string +} + +interface SessionState { + currentUser: MockUser | null + activeOrganizationId: string | null + isAuthenticated: boolean + login: (user: MockUser) => void + logout: () => void + switchOrganization: (organizationId: string) => void +} + +const mockUser: MockUser = { + id: 'user-001', + email: 'admin@ideal-sharing.ch', + name: 'Admin User', + role: UserRole.ORGANIZATION_ADMIN, + organizationId: 'org-wincasa', + organizationName: 'Wincasa AG', +} + +export const useSessionStore = create((set) => ({ + currentUser: mockUser, + activeOrganizationId: mockUser.organizationId, + isAuthenticated: true, + login: (user) => set({ currentUser: user, activeOrganizationId: user.organizationId, isAuthenticated: true }), + logout: () => set({ currentUser: null, activeOrganizationId: null, isAuthenticated: false }), + switchOrganization: (organizationId) => set({ activeOrganizationId: organizationId }), +})) diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..7f42e5f --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..c676acd --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + plugins: [ + react(), + tailwindcss(), + ], +})