diff --git a/.claude/worktrees/agent-a82a3716/.gitignore b/.claude/worktrees/agent-a82a3716/.gitignore deleted file mode 100644 index e5b8580..0000000 --- a/.claude/worktrees/agent-a82a3716/.gitignore +++ /dev/null @@ -1,28 +0,0 @@ -# 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/worktrees/agent-a82a3716/CLAUDE.md b/.claude/worktrees/agent-a82a3716/CLAUDE.md deleted file mode 100644 index 01a1e3b..0000000 --- a/.claude/worktrees/agent-a82a3716/CLAUDE.md +++ /dev/null @@ -1,80 +0,0 @@ -# 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/.claude/worktrees/agent-a82a3716/README.md b/.claude/worktrees/agent-a82a3716/README.md deleted file mode 100644 index 7dbf7eb..0000000 --- a/.claude/worktrees/agent-a82a3716/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# 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/.claude/worktrees/agent-a82a3716/eslint.config.js b/.claude/worktrees/agent-a82a3716/eslint.config.js deleted file mode 100644 index ef614d2..0000000 --- a/.claude/worktrees/agent-a82a3716/eslint.config.js +++ /dev/null @@ -1,22 +0,0 @@ -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/.claude/worktrees/agent-a82a3716/index.html b/.claude/worktrees/agent-a82a3716/index.html deleted file mode 100644 index b0e5ae0..0000000 --- a/.claude/worktrees/agent-a82a3716/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Property Match - - -
- - - diff --git a/.claude/worktrees/agent-a82a3716/package-lock.json b/.claude/worktrees/agent-a82a3716/package-lock.json deleted file mode 100644 index 9aa405f..0000000 --- a/.claude/worktrees/agent-a82a3716/package-lock.json +++ /dev/null @@ -1,3942 +0,0 @@ -{ - "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/.claude/worktrees/agent-a82a3716/package.json b/.claude/worktrees/agent-a82a3716/package.json deleted file mode 100644 index d26f231..0000000 --- a/.claude/worktrees/agent-a82a3716/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "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/.claude/worktrees/agent-a82a3716/public/favicon.svg b/.claude/worktrees/agent-a82a3716/public/favicon.svg deleted file mode 100644 index 6893eb1..0000000 --- a/.claude/worktrees/agent-a82a3716/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/.claude/worktrees/agent-a82a3716/public/icons.svg b/.claude/worktrees/agent-a82a3716/public/icons.svg deleted file mode 100644 index e952219..0000000 --- a/.claude/worktrees/agent-a82a3716/public/icons.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/worktrees/agent-a82a3716/src/App.css b/.claude/worktrees/agent-a82a3716/src/App.css deleted file mode 100644 index f90339d..0000000 --- a/.claude/worktrees/agent-a82a3716/src/App.css +++ /dev/null @@ -1,184 +0,0 @@ -.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/.claude/worktrees/agent-a82a3716/src/App.tsx b/.claude/worktrees/agent-a82a3716/src/App.tsx deleted file mode 100644 index 908bc7e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/App.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { lazy, Suspense } from 'react' -import { Routes, Route, Navigate } from 'react-router' -import { LoadingPage, AppErrorBoundary } from './components/ui' -import { AppShell } from './components/layout' -import { ProtectedRoute } from './components/auth' -import { WorkspaceType } from './domain/enums' -import { useSessionStore } from './stores/sessionStore' - -const WORKSPACE_HOME: Record = { - [WorkspaceType.SUPPLY]: '/supply/dashboard', - [WorkspaceType.DEMAND]: '/demand/ai-search', - [WorkspaceType.OPERATIONS]: '/ops/review-queue', -} - -function RoleRedirect() { - const { currentUser } = useSessionStore() - const first = currentUser?.allowedWorkspaces[0] ?? WorkspaceType.SUPPLY - return -} - -const LoginScreen = lazy(() => import('./pages/auth/LoginScreen')) - -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 MatchDetail = lazy(() => import('./pages/demand/MatchDetail')) -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')) -const MarketIntelligence = lazy(() => import('./pages/ops/MarketIntelligence')) -const SourceMonitoring = lazy(() => import('./pages/ops/SourceMonitoring')) -const ActivityTimeline = lazy(() => import('./pages/ops/ActivityTimeline')) -const SignalPipeline = lazy(() => import('./pages/ops/SignalPipeline')) - -function App() { - return ( - - }> - - {/* Public */} - } /> - - {/* Protected: auth check only */} - }> - }> - } /> - - {/* Supply Workspace */} - }> - } /> - } /> - } /> - } /> - } /> - - - {/* Demand Workspace */} - }> - } /> - } /> - } /> - } /> - } /> - - - {/* Operations Workspace */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - - } /> - - - - ) -} - -export default App diff --git a/.claude/worktrees/agent-a82a3716/src/assets/hero.png b/.claude/worktrees/agent-a82a3716/src/assets/hero.png deleted file mode 100644 index 02251f4..0000000 Binary files a/.claude/worktrees/agent-a82a3716/src/assets/hero.png and /dev/null differ diff --git a/.claude/worktrees/agent-a82a3716/src/assets/react.svg b/.claude/worktrees/agent-a82a3716/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/.claude/worktrees/agent-a82a3716/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/.claude/worktrees/agent-a82a3716/src/assets/vite.svg b/.claude/worktrees/agent-a82a3716/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/.claude/worktrees/agent-a82a3716/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIErrorBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIErrorBadge.tsx deleted file mode 100644 index d063d8b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIErrorBadge.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Chip, Tooltip } from '@mui/material' -import type { AIOutputError } from '../../domain/aiOutput' - -const ERROR_CONFIG: Record = { - SCHEMA_VALIDATION: { label: 'Schema', color: '#ea580c' }, - PROVIDER_TIMEOUT: { label: 'Timeout', color: '#c0392b' }, - INVALID_JSON: { label: 'JSON', color: '#c0392b' }, - EMPTY_RESPONSE: { label: 'Leer', color: '#d97706' }, - RATE_LIMIT: { label: 'Rate Limit', color: '#7c3aed' }, -} - -export function AIErrorBadge({ error }: { error: AIOutputError }) { - const { label, color } = ERROR_CONFIG[error.type] ?? { label: error.type, color: '#c0392b' } - return ( - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIMonitoringEmptyState.tsx b/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIMonitoringEmptyState.tsx deleted file mode 100644 index 3c598d7..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIMonitoringEmptyState.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Box, Typography } from '@mui/material' -import { Bot, Filter, MousePointer } from 'lucide-react' - -interface Props { - context: 'no-outputs' | 'filtered-empty' | 'no-selection' -} - -const CONFIG = { - 'no-outputs': { - icon: Bot, - color: '#94a3b8', - title: 'Keine AI-Outputs', - desc: 'Es wurden noch keine AI-Outputs generiert.', - }, - 'filtered-empty': { - icon: Filter, - color: '#94a3b8', - title: 'Keine Ergebnisse', - desc: 'Kein AI-Output entspricht den aktiven Filtern.', - }, - 'no-selection': { - icon: MousePointer, - color: '#94a3b8', - title: 'Output auswählen', - desc: 'Klicken Sie auf eine Zeile, um Details und Aktionen anzuzeigen.', - }, -} - -export function AIMonitoringEmptyState({ context }: Props) { - const { icon: Icon, color, title, desc } = CONFIG[context] - return ( - - - {title} - {desc} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIMonitoringMetrics.tsx b/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIMonitoringMetrics.tsx deleted file mode 100644 index 8c051b0..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIMonitoringMetrics.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Box, Typography } from '@mui/material' -import type { AIOutput } from '../../domain/aiOutput' - -interface Props { - outputs: AIOutput[] -} - -function MetricCell({ label, value, color }: { label: string; value: string | number; color?: string }) { - return ( - - - {label} - - - {value} - - - ) -} - -function mostCommon(arr: string[]): string { - if (!arr.length) return '–' - const freq = arr.reduce>((acc, v) => ({ ...acc, [v]: (acc[v] ?? 0) + 1 }), {}) - return Object.entries(freq).sort((a, b) => b[1] - a[1])[0][0] -} - -export function AIMonitoringMetrics({ outputs }: Props) { - const total = outputs.length - const failed = outputs.filter(o => !!o.error).length - const needsReview = outputs.filter(o => o.reviewStatus === 'UNREVIEWED' || o.reviewStatus === 'FLAGGED').length - const approved = outputs.filter(o => o.reviewStatus === 'APPROVED').length - const approvalRate = total > 0 ? Math.round((approved / total) * 100) : 0 - const topPrompt = mostCommon(outputs.map(o => o.promptVersion)) - const latestModel = outputs.length > 0 - ? outputs.sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0].model - : '–' - - return ( - - - 0 ? '#c0392b' : undefined} /> - 0 ? '#d97706' : undefined} /> - = 70 ? '#1a7a4a' : '#d97706'} /> - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIOutputDetailPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIOutputDetailPanel.tsx deleted file mode 100644 index d5869f5..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIOutputDetailPanel.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { Alert, Box, Divider, IconButton, Typography } from '@mui/material' -import { X } from 'lucide-react' -import { AIOutputStatusBadge } from './AIOutputStatusBadge' -import { PromptVersionBadge } from './PromptVersionBadge' -import { AIErrorBadge } from './AIErrorBadge' -import { AIReviewActionToolbar } from './AIReviewActionToolbar' -import type { AIOutput, AIOutputType } from '../../domain/aiOutput' -import type { ReviewStatus } from '../../domain/enums' - -const TYPE_LABELS: Record = { - NEED_PARSE: 'Bedarf-Parsing', - FOLLOW_UP_QUESTIONS: 'Rückfragen', - MATCH_EXPLANATION: 'Match-Begründung', - COMPARE_SUMMARY: 'Vergleich', - DECISION_BRIEF: 'Entscheidungs-Brief', - DATA_QUALITY_SUMMARY: 'Datenqualität', -} - -const ERROR_TYPE_LABELS: Record = { - SCHEMA_VALIDATION: 'Schema-Validierungsfehler', - PROVIDER_TIMEOUT: 'Provider-Timeout', - INVALID_JSON: 'Ungültiges JSON', - EMPTY_RESPONSE: 'Leere Antwort', - RATE_LIMIT: 'Rate-Limit erreicht', -} - -const MODEL_LABELS: Record = { - 'claude-3-5-sonnet-20241022': 'Claude 3.5 Sonnet', - 'claude-3-haiku-20240307': 'Claude 3 Haiku', - 'claude-3-opus-20240229': 'Claude 3 Opus', -} - -const ENTITY_TYPE_LABELS: Record = { - NEED: 'Gesuch', MATCH: 'Match', PROPERTY: 'Objekt', SIGNAL: 'Signal', -} - -interface Props { - output: AIOutput - onClose: () => void - onUpdateStatus: (status: ReviewStatus) => void - isSubmitting?: boolean -} - -function MetaRow({ label, children }: { label: string; children: React.ReactNode }) { - return ( - - - {label} - - {children} - - ) -} - -export function AIOutputDetailPanel({ output, onClose, onUpdateStatus, isSubmitting }: Props) { - const handleCopyJson = () => { - navigator.clipboard.writeText(output.outputPreview).catch(() => {}) - } - - return ( - - {/* Header */} - - - - - - {output.error && } - - - {TYPE_LABELS[output.type] ?? output.type} - - - - - - - - - {/* Scrollable body */} - - {/* Metadata */} - - - {output.id} - - - - - {new Date(output.createdAt).toLocaleString('de-CH', { dateStyle: 'medium', timeStyle: 'short' })} - - - - - {MODEL_LABELS[output.model] ?? output.model} - - - - - {output.provider} - - - - - - - - {output.inputHash} - - - - - {ENTITY_TYPE_LABELS[output.relatedEntityType] ?? output.relatedEntityType}{' '} - - {output.relatedEntityId} - - - - {output.latencyMs != null && ( - - 5000 ? '#c0392b' : '#334155', fontWeight: output.latencyMs > 5000 ? 700 : 400 }}> - {(output.latencyMs / 1000).toFixed(2)}s - - - )} - {output.costEstimate != null && ( - - - ${output.costEstimate.toFixed(4)} - - - )} - - - - {/* Error details */} - {output.error && ( - - - {ERROR_TYPE_LABELS[output.error.type] ?? output.error.type} -
- {output.error.message} - {output.error.recoverable && ( - - Wiederholbar — kann erneut ausgelöst werden. - - )} -
-
- )} - - {/* Output preview */} - - - Output-Vorschau - - - {output.outputPreview || '(kein Output)'} - - - - - - {/* Actions */} - -
-
- ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIOutputStatusBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIOutputStatusBadge.tsx deleted file mode 100644 index 409d3c0..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIOutputStatusBadge.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Chip } from '@mui/material' -import type { ReviewStatus } from '../../domain/enums' - -const CONFIG: Record = { - UNREVIEWED: { label: 'Ungeprüft', color: '#94a3b8' }, - IN_REVIEW: { label: 'In Prüfung', color: '#d97706' }, - APPROVED: { label: 'Genehmigt', color: '#1a7a4a' }, - REJECTED: { label: 'Abgelehnt', color: '#c0392b' }, - FLAGGED: { label: 'Markiert', color: '#ea580c' }, -} - -export function AIOutputStatusBadge({ status }: { status: ReviewStatus }) { - const { label, color } = CONFIG[status] ?? { label: status, color: '#64748b' } - return ( - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIOutputTable.tsx b/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIOutputTable.tsx deleted file mode 100644 index 1a6e9a0..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIOutputTable.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { - Box, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Typography, -} from '@mui/material' -import { AIOutputStatusBadge } from './AIOutputStatusBadge' -import { PromptVersionBadge } from './PromptVersionBadge' -import { AIErrorBadge } from './AIErrorBadge' -import { AIMonitoringEmptyState } from './AIMonitoringEmptyState' -import type { AIOutput, AIOutputType } from '../../domain/aiOutput' - -const TYPE_LABELS: Record = { - NEED_PARSE: 'Bedarf-Parsing', - FOLLOW_UP_QUESTIONS: 'Rückfragen', - MATCH_EXPLANATION: 'Match-Begründung', - COMPARE_SUMMARY: 'Vergleich', - DECISION_BRIEF: 'Entscheidungs-Brief', - DATA_QUALITY_SUMMARY: 'Datenqualität', -} - -const TYPE_COLORS: Record = { - NEED_PARSE: '#1e3a5f', - FOLLOW_UP_QUESTIONS: '#0891b2', - MATCH_EXPLANATION: '#4f46e5', - COMPARE_SUMMARY: '#1a7a4a', - DECISION_BRIEF: '#7c3aed', - DATA_QUALITY_SUMMARY: '#d97706', -} - -const MODEL_SHORT: Record = { - 'claude-3-5-sonnet-20241022': 'Sonnet 3.5', - 'claude-3-haiku-20240307': 'Haiku 3', - 'claude-3-opus-20240229': 'Opus 3', -} - -function shortTime(iso: string) { - return new Date(iso).toLocaleString('de-CH', { dateStyle: 'short', timeStyle: 'short' }) -} - -interface Props { - outputs: AIOutput[] - selectedId: string | null - onSelect: (output: AIOutput) => void - isEmpty: boolean -} - -export function AIOutputTable({ outputs, selectedId, onSelect, isEmpty }: Props) { - if (isEmpty && outputs.length === 0) { - return - } - if (outputs.length === 0) { - return - } - - return ( - - - - Zeitpunkt - Typ - Modell - Version - Status - Latenz - Fehler - - - - {outputs.map(output => { - const isSelected = selectedId === output.id - const color = TYPE_COLORS[output.type] ?? '#64748b' - return ( - onSelect(output)} - sx={{ - cursor: 'pointer', - bgcolor: isSelected ? '#eff6ff' : undefined, - borderLeft: isSelected ? '3px solid #1e3a5f' : '3px solid transparent', - '&:hover': { bgcolor: isSelected ? '#eff6ff' : '#f8fafc' }, - '& td': { py: 0.75, borderBottom: '1px solid #f1f5f9' }, - }} - > - - - {shortTime(output.createdAt)} - - - - - - {TYPE_LABELS[output.type] ?? output.type} - - - - - - {MODEL_SHORT[output.model] ?? output.model} - - - - - - - - - - 5000 ? '#c0392b' : '#64748b' }}> - {output.latencyMs != null ? `${(output.latencyMs / 1000).toFixed(1)}s` : '–'} - - - - {output.error ? : ( - - )} - - - ) - })} - -
- ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIReviewActionToolbar.tsx b/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIReviewActionToolbar.tsx deleted file mode 100644 index 8ab9a59..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIReviewActionToolbar.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Box, Button, Tooltip } from '@mui/material' -import { CheckCircle, XCircle, Send, Copy } from 'lucide-react' -import type { AIOutput } from '../../domain/aiOutput' -import type { ReviewStatus } from '../../domain/enums' - -interface Props { - output: AIOutput - onUpdateStatus: (status: ReviewStatus) => void - onCopyJson: () => void - isSubmitting?: boolean -} - -export function AIReviewActionToolbar({ output, onUpdateStatus, onCopyJson, isSubmitting }: Props) { - const { reviewStatus } = output - - const canSendToReview = reviewStatus === 'UNREVIEWED' || reviewStatus === 'FLAGGED' - const canApprove = reviewStatus === 'IN_REVIEW' || reviewStatus === 'UNREVIEWED' - const canReject = reviewStatus !== 'REJECTED' - - return ( - - {canSendToReview && ( - - )} - {canApprove && ( - - )} - {canReject && ( - - )} - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/PromptVersionBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/PromptVersionBadge.tsx deleted file mode 100644 index f675db7..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/PromptVersionBadge.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Box, Tooltip, Typography } from '@mui/material' - -interface Props { - promptVersion: string - schemaVersion?: string -} - -export function PromptVersionBadge({ promptVersion, schemaVersion }: Props) { - const badge = ( - - - {promptVersion} - - {schemaVersion && ( - <> - - - {schemaVersion} - - - )} - - ) - - return schemaVersion ? ( - {badge} - ) : badge -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/index.ts b/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/index.ts deleted file mode 100644 index 27ab0b5..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { AIOutputStatusBadge } from './AIOutputStatusBadge' -export { PromptVersionBadge } from './PromptVersionBadge' -export { AIErrorBadge } from './AIErrorBadge' -export { AIMonitoringEmptyState } from './AIMonitoringEmptyState' -export { AIMonitoringMetrics } from './AIMonitoringMetrics' -export { AIOutputTable } from './AIOutputTable' -export { AIReviewActionToolbar } from './AIReviewActionToolbar' -export { AIOutputDetailPanel } from './AIOutputDetailPanel' diff --git a/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantActionCards.tsx b/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantActionCards.tsx deleted file mode 100644 index c2dfbeb..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantActionCards.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { Box, Button, Typography } from '@mui/material' -import { ArrowRight } from 'lucide-react' -import { useNavigate } from 'react-router' -import type { AssistantAction } from '../../domain/assistant' - -interface Props { - actions: AssistantAction[] - onExecute?: (action: AssistantAction) => void -} - -const ACTION_COLORS: Record = { - NAVIGATE: '#1e3a5f', - OPEN_REVIEW: '#7c3aed', - ADD_TO_SHORTLIST: '#1a7a4a', - REQUEST_DATA: '#d97706', - SEND_TO_REVIEW: '#ea580c', -} - -export function AssistantActionCards({ actions, onExecute }: Props) { - const navigate = useNavigate() - - const handleExecute = (action: AssistantAction) => { - if (action.actionType === 'NAVIGATE' && action.payload?.path) { - navigate(action.payload.path as string) - } else if (action.actionType === 'OPEN_REVIEW') { - navigate('/ops/review-queue') - } - onExecute?.(action) - } - - if (!actions.length) return null - - return ( - - - Vorgeschlagene Aktionen - - - {actions.map(action => { - const color = ACTION_COLORS[action.actionType] ?? '#64748b' - return ( - - - - {action.label} - - - {action.description} - - - - - ) - })} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantContextSummary.tsx b/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantContextSummary.tsx deleted file mode 100644 index a6637cf..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantContextSummary.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Box, Chip, Typography } from '@mui/material' -import type { AssistantContext } from '../../domain/assistant' - -const PAGE_LABELS: Record = { - '/supply/dashboard': 'Übersicht', - '/supply/properties': 'Meine Objekte', - '/supply/match-center': 'Eingehende Bedarfe', - '/supply/data-quality': 'Datenpflege', - '/supply/future-availability':'Marktchancen', - '/demand/ai-search': 'Flächensuche', - '/demand/results': 'Ergebnisse', - '/demand/compare': 'Vergleich', - '/demand/shortlists': 'Shortlists', - '/ops/review-queue': 'Review Queue', - '/ops/ai-monitoring': 'AI Monitoring', - '/ops/governance': 'Governance', -} - -function resolvePageLabel(route: string): string { - for (const [path, label] of Object.entries(PAGE_LABELS)) { - if (route.startsWith(path)) return label - } - return route.split('/').filter(Boolean).pop()?.replace(/-/g, ' ') ?? 'Seite' -} - -const ENTITY_LABELS: Record = { - PROPERTY: 'Objekt', NEED: 'Gesuch', MATCH: 'Match', - SIGNAL: 'Signal', AI_OUTPUT: 'AI-Output', -} - -interface Props { - context: AssistantContext -} - -export function AssistantContextSummary({ context }: Props) { - const pageLabel = resolvePageLabel(context.currentRoute) - - return ( - - - - {context.selectedEntityType && context.selectedEntityId && ( - - )} - {context.visibleScores?.quality !== undefined && ( - = 70 ? '#dcfce7' : '#fef3c7', - color: context.visibleScores.quality >= 70 ? '#166534' : '#92400e', - fontWeight: 600, fontSize: '0.65rem', height: 20, - }} - /> - )} - {context.visibleScores?.matchScore !== undefined && ( - - )} - - {context.visibleMissingData && context.visibleMissingData.length > 0 && ( - - Fehlende Daten: {context.visibleMissingData.slice(0, 3).join(', ')} - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantErrorState.tsx b/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantErrorState.tsx deleted file mode 100644 index a18c867..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantErrorState.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Alert, Box, Button } from '@mui/material' -import { RefreshCw } from 'lucide-react' - -interface Props { - error: string - onRetry?: () => void -} - -export function AssistantErrorState({ error, onRetry }: Props) { - return ( - - } sx={{ textTransform: 'none', fontSize: '0.75rem' }}> - Erneut - - ) : undefined - } - > - {error} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantLoadingState.tsx b/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantLoadingState.tsx deleted file mode 100644 index 67514b4..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantLoadingState.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Box, Typography } from '@mui/material' - -export function AssistantLoadingState() { - return ( - - - AI - - - {[0, 1, 2].map(i => ( - - ))} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantMessageList.tsx b/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantMessageList.tsx deleted file mode 100644 index 858c493..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantMessageList.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Box, Typography } from '@mui/material' -import { AssistantActionCards } from './AssistantActionCards' -import type { AssistantMessage } from '../../domain/assistant' - -function MessageBubble({ message }: { message: AssistantMessage }) { - const isUser = message.role === 'user' - - return ( - - {!isUser && ( - - AI - - )} - - - {/* Bubble */} - - $1') - .replace(/\n/g, '
'), - }} - /> -
- - {/* Metadata */} - {!isUser && (message.confidence !== undefined || (message.sources && message.sources.length > 0)) && ( - - {message.confidence !== undefined && ( - - Konfidenz: {Math.round(message.confidence * 100)}% - - )} - {message.sources?.map(s => ( - - {s} - - ))} - - )} - - {/* Timestamp */} - - {new Date(message.createdAt).toLocaleTimeString('de-CH', { timeStyle: 'short' })} - -
-
- ) -} - -interface Props { - messages: AssistantMessage[] -} - -export function AssistantMessageList({ messages }: Props) { - return ( - - {messages.map((msg) => ( - - - {msg.role === 'assistant' && msg.actions && msg.actions.length > 0 && ( - - )} - - ))} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantPromptSuggestions.tsx b/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantPromptSuggestions.tsx deleted file mode 100644 index 9608b23..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/assistant/AssistantPromptSuggestions.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Box, Chip, Typography } from '@mui/material' -import type { SuggestedQuestion } from '../../domain/assistant' - -interface Props { - suggestions: SuggestedQuestion[] - onSelect: (question: string) => void - disabled?: boolean -} - -const CATEGORY_COLORS: Record = { - Match: '#4f46e5', - Datenqualität: '#d97706', - Priorisierung: '#1e3a5f', - Empfehlung: '#1a7a4a', - Risiko: '#c0392b', - Tradeoffs: '#ea580c', - Strategie: '#0891b2', - Analyse: '#7c3aed', - Erklärung: '#0891b2', - Evidenz: '#64748b', - Review: '#7c3aed', - Konfidenz: '#d97706', - Fehler: '#c0392b', - Fehleranalyse: '#ea580c', - Eskalation: '#ea580c', - Prozess: '#64748b', - Kosten: '#1a7a4a', - Impact: '#d97706', - Optimierung: '#1a7a4a', - Aktion: '#1e3a5f', - Überblick: '#64748b', - Ranking: '#4f46e5', -} - -export function AssistantPromptSuggestions({ suggestions, onSelect, disabled }: Props) { - if (suggestions.length === 0) return null - - return ( - - - Vorschläge - - - {suggestions.map(s => { - const catColor = CATEGORY_COLORS[s.category] ?? '#64748b' - return ( - !disabled && onSelect(s.question)} - sx={{ - px: 1.25, - py: 0.875, - borderRadius: 1.5, - border: '1px solid #e2e8f0', - cursor: disabled ? 'default' : 'pointer', - bgcolor: 'white', - opacity: disabled ? 0.5 : 1, - '&:hover': disabled ? {} : { bgcolor: '#f8fafc', borderColor: '#cbd5e1' }, - transition: 'all 0.1s ease', - display: 'flex', - alignItems: 'center', - gap: 1, - }} - > - - - - {s.question} - - - - - ) - })} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/assistant/GlobalAIAssistantButton.tsx b/.claude/worktrees/agent-a82a3716/src/components/assistant/GlobalAIAssistantButton.tsx deleted file mode 100644 index 94db088..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/assistant/GlobalAIAssistantButton.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Box, IconButton, Tooltip } from '@mui/material' -import { Sparkles } from 'lucide-react' -import { useAssistantStore } from '../../stores/assistantStore' - -export function GlobalAIAssistantButton() { - const { isOpen, open } = useAssistantStore() - - return ( - - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/assistant/GlobalAIAssistantDrawer.tsx b/.claude/worktrees/agent-a82a3716/src/components/assistant/GlobalAIAssistantDrawer.tsx deleted file mode 100644 index ddec7ec..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/assistant/GlobalAIAssistantDrawer.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react' -import { Box, Divider, Drawer, IconButton, TextField, Tooltip, Typography } from '@mui/material' -import { RotateCcw, Send, Sparkles, X } from 'lucide-react' -import { useLocation } from 'react-router' -import { useAssistantStore } from '../../stores/assistantStore' -import { useSessionStore } from '../../stores/sessionStore' -import { aiAssistantService } from '../../services/aiAssistantService' -import { AssistantContextSummary } from './AssistantContextSummary' -import { AssistantMessageList } from './AssistantMessageList' -import { AssistantPromptSuggestions } from './AssistantPromptSuggestions' -import { AssistantLoadingState } from './AssistantLoadingState' -import { AssistantErrorState } from './AssistantErrorState' -import type { AssistantContext, SuggestedQuestion } from '../../domain/assistant' -import type { WorkspaceType } from '../../domain/enums' - -function resolveWorkspace(pathname: string): WorkspaceType | null { - if (pathname.startsWith('/supply')) return 'SUPPLY' as WorkspaceType - if (pathname.startsWith('/demand')) return 'DEMAND' as WorkspaceType - if (pathname.startsWith('/ops')) return 'OPERATIONS' as WorkspaceType - return null -} - -export function GlobalAIAssistantDrawer() { - const { isOpen, close, context, setContext, messages, isLoading, error, addMessage, setLoading, setError, clearConversation } = - useAssistantStore() - - const { currentUser } = useSessionStore() - const location = useLocation() - - const [suggestions, setSuggestions] = useState([]) - const [inputText, setInputText] = useState('') - const scrollRef = useRef(null) - - // Build context from route when drawer opens - useEffect(() => { - if (!isOpen) return - const ctx: AssistantContext = { - currentRoute: location.pathname, - workspace: resolveWorkspace(location.pathname), - userRole: currentUser?.role ?? 'VIEWER', - organizationId: currentUser?.organizationId ?? '', - } - setContext(ctx) - aiAssistantService.getSuggestions(ctx).then(setSuggestions) - }, [isOpen, location.pathname]) - - // Refresh suggestions when route changes while open - useEffect(() => { - if (!isOpen) return - const ctx: AssistantContext = { - currentRoute: location.pathname, - workspace: resolveWorkspace(location.pathname), - userRole: currentUser?.role ?? 'VIEWER', - organizationId: currentUser?.organizationId ?? '', - } - setContext(ctx) - aiAssistantService.getSuggestions(ctx).then(setSuggestions) - }, [location.pathname]) - - // Auto-scroll on new messages - useEffect(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight - } - }, [messages, isLoading]) - - const handleQuestion = useCallback(async (question: string) => { - if (!question.trim() || isLoading) return - setInputText('') - setError(null) - - const userMsg = { - id: crypto.randomUUID(), - role: 'user' as const, - content: question.trim(), - createdAt: new Date().toISOString(), - } - addMessage(userMsg) - setLoading(true) - - try { - const ctx = context ?? { - currentRoute: location.pathname, - workspace: resolveWorkspace(location.pathname), - userRole: currentUser?.role ?? 'VIEWER', - organizationId: currentUser?.organizationId ?? '', - } - const answer = await aiAssistantService.answerQuestion(ctx, question) - addMessage({ - id: crypto.randomUUID(), - role: 'assistant', - createdAt: new Date().toISOString(), - ...answer, - }) - } catch { - setError('Antwort konnte nicht generiert werden. Bitte erneut versuchen.') - } finally { - setLoading(false) - } - }, [context, isLoading, location.pathname, currentUser, addMessage, setLoading, setError]) - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault() - handleQuestion(inputText) - } - } - - const handleClear = () => { - clearConversation() - setSuggestions([]) - if (context) { - aiAssistantService.getSuggestions(context).then(setSuggestions) - } - } - - const showSuggestions = suggestions.length > 0 && messages.length === 0 - - return ( - - {/* Header */} - - - - - - - AI Assistent - - - Kontextbasierte Entscheidungsunterstützung - - - - - - - - - - - - - {/* Context summary */} - {context && } - - {/* Scrollable body */} - - {/* Welcome message */} - {messages.length === 0 && !isLoading && ( - - - - Ich helfe Ihnen mit kontextbezogenen Fragen zu dieser Seite. Meine Antworten basieren auf strukturierten Daten — keine erfundenen Fakten. - - - - )} - - {/* Suggestions */} - {showSuggestions && ( - <> - - - - )} - - {/* Messages */} - {messages.length > 0 && ( - - - - )} - - {/* Inline suggestions after messages */} - {messages.length > 0 && suggestions.length > 0 && !isLoading && ( - <> - - - - )} - - {/* Loading */} - {isLoading && } - - {/* Error */} - {error && setError(null)} />} - - - {/* Input area */} - - - setInputText(e.target.value)} - onKeyDown={handleKeyDown} - disabled={isLoading} - sx={{ - '& .MuiOutlinedInput-root': { fontSize: '0.8125rem', borderRadius: 2 }, - }} - /> - - - handleQuestion(inputText)} - disabled={!inputText.trim() || isLoading} - sx={{ - bgcolor: '#4f46e5', - color: 'white', - flexShrink: 0, - '&:hover': { bgcolor: '#4338ca' }, - '&:disabled': { bgcolor: '#e2e8f0', color: '#94a3b8' }, - }} - > - - - - - - - Antworten sind datenbasiert — Aktionen erfordern manuelle Bestätigung - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/assistant/index.ts b/.claude/worktrees/agent-a82a3716/src/components/assistant/index.ts deleted file mode 100644 index 8f3e084..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/assistant/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { GlobalAIAssistantButton } from './GlobalAIAssistantButton' -export { GlobalAIAssistantDrawer } from './GlobalAIAssistantDrawer' -export { AssistantMessageList } from './AssistantMessageList' -export { AssistantPromptSuggestions } from './AssistantPromptSuggestions' -export { AssistantContextSummary } from './AssistantContextSummary' -export { AssistantActionCards } from './AssistantActionCards' -export { AssistantLoadingState } from './AssistantLoadingState' -export { AssistantErrorState } from './AssistantErrorState' diff --git a/.claude/worktrees/agent-a82a3716/src/components/auth/AccessDenied.tsx b/.claude/worktrees/agent-a82a3716/src/components/auth/AccessDenied.tsx deleted file mode 100644 index e15dfc4..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/auth/AccessDenied.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Box, Button, Typography } from '@mui/material' -import type { SxProps, Theme } from '@mui/material' -import { ShieldOff } from 'lucide-react' -import { useNavigate } from 'react-router' - -interface AccessDeniedProps { - title?: string - message?: string - onBack?: () => void - sx?: SxProps -} - -export function AccessDenied({ - title = 'Kein Zugriff', - message = 'Sie haben keine Berechtigung, diesen Bereich zu öffnen.', - onBack, - sx, -}: AccessDeniedProps) { - const navigate = useNavigate() - - function handleBack() { - if (onBack) { - onBack() - } else { - navigate(-1) - } - } - - return ( - - - - - - - - {title} - - - {message} - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/auth/DemoRoleSwitcher.tsx b/.claude/worktrees/agent-a82a3716/src/components/auth/DemoRoleSwitcher.tsx deleted file mode 100644 index bd8ddd6..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/auth/DemoRoleSwitcher.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Box, Chip, Typography } from '@mui/material' -import { UserRole } from '../../domain/enums' -import { authService } from '../../services/authService' -import { useSessionStore } from '../../stores/sessionStore' - -const ROLE_LABELS: Record = { - [UserRole.SUPER_ADMIN]: 'Super Admin', - [UserRole.ORGANIZATION_ADMIN]: 'Org Admin', - [UserRole.PROPERTY_MANAGER]: 'Prop. Manager', - [UserRole.REVIEWER]: 'Reviewer', - [UserRole.OWNER_VIEWER]: 'Owner Viewer', - [UserRole.DEMAND_USER]: 'Demand User', -} - -export function DemoRoleSwitcher() { - const { currentUser } = useSessionStore() - - async function handleSwitch(role: UserRole) { - await authService.switchDemoRole(role) - } - - return ( - - - Demo-Modus - - - {Object.values(UserRole).map((role) => { - const active = currentUser?.role === role - return ( - handleSwitch(role)} - sx={{ - fontSize: '0.7rem', - height: 22, - bgcolor: active ? '#1e3a5f' : 'transparent', - color: active ? '#fff' : 'text.secondary', - border: '1px solid', - borderColor: active ? '#1e3a5f' : 'divider', - '&:hover': { bgcolor: active ? '#162d4a' : 'rgba(0,0,0,0.04)' }, - }} - /> - ) - })} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/auth/OrganizationSwitcher.tsx b/.claude/worktrees/agent-a82a3716/src/components/auth/OrganizationSwitcher.tsx deleted file mode 100644 index 56e0193..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/auth/OrganizationSwitcher.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { FormControl, MenuItem, Select, Typography } from '@mui/material' -import type { SelectChangeEvent } from '@mui/material' -import { authService } from '../../services/authService' -import { useSessionStore } from '../../stores/sessionStore' - -const MOCK_ORGANIZATIONS = [ - { id: 'org-wincasa', name: 'Wincasa AG' }, - { id: 'org-mobimo', name: 'Mobimo Management AG' }, - { id: 'org-ubs', name: 'UBS Asset Management RE' }, -] - -export function OrganizationSwitcher() { - const { activeOrganizationId } = useSessionStore() - - async function handleChange(e: SelectChangeEvent) { - await authService.switchOrganization(e.target.value) - } - - return ( - - - Organisation - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/auth/PermissionGate.tsx b/.claude/worktrees/agent-a82a3716/src/components/auth/PermissionGate.tsx deleted file mode 100644 index edd8ccb..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/auth/PermissionGate.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { ReactNode } from 'react' -import type { MockUser } from '../../stores/sessionStore' -import { useSessionStore } from '../../stores/sessionStore' - -interface PermissionGateProps { - check: (user: MockUser) => boolean - fallback?: ReactNode - children: ReactNode -} - -export function PermissionGate({ check, fallback = null, children }: PermissionGateProps) { - const { currentUser } = useSessionStore() - - if (!currentUser || !check(currentUser)) { - return <>{fallback} - } - - return <>{children} -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/auth/ProtectedRoute.tsx b/.claude/worktrees/agent-a82a3716/src/components/auth/ProtectedRoute.tsx deleted file mode 100644 index a8007f6..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/auth/ProtectedRoute.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Navigate, Outlet } from 'react-router' -import type { WorkspaceType } from '../../domain/enums' -import { useSessionStore } from '../../stores/sessionStore' -import { SessionStatus } from '../../stores/sessionStore' -import { canAccessWorkspace } from '../../lib/permissions' -import { AccessDenied } from './AccessDenied' -import { SessionExpired } from './SessionExpired' - -interface ProtectedRouteProps { - workspace?: WorkspaceType -} - -export function ProtectedRoute({ workspace }: ProtectedRouteProps) { - const { isAuthenticated, currentUser, sessionStatus } = useSessionStore() - - if (!isAuthenticated || sessionStatus === SessionStatus.UNAUTHENTICATED) { - return - } - - if (sessionStatus === SessionStatus.EXPIRED) { - return - } - - if (workspace && currentUser && !canAccessWorkspace(currentUser, workspace)) { - const workspaceLabel: Record = { - SUPPLY: 'Supply', - DEMAND: 'Demand', - OPERATIONS: 'Operations', - } - return ( - - ) - } - - return -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/auth/RoleGuard.tsx b/.claude/worktrees/agent-a82a3716/src/components/auth/RoleGuard.tsx deleted file mode 100644 index 577c360..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/auth/RoleGuard.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { ReactNode } from 'react' -import type { UserRole } from '../../domain/enums' -import { useSessionStore } from '../../stores/sessionStore' -import { AccessDenied } from './AccessDenied' - -interface RoleGuardProps { - roles: UserRole[] - fallback?: ReactNode - children: ReactNode -} - -export function RoleGuard({ roles, fallback, children }: RoleGuardProps) { - const { currentUser } = useSessionStore() - - if (!currentUser || !roles.includes(currentUser.role)) { - return <>{fallback ?? } - } - - return <>{children} -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/auth/SessionExpired.tsx b/.claude/worktrees/agent-a82a3716/src/components/auth/SessionExpired.tsx deleted file mode 100644 index 0ede7c4..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/auth/SessionExpired.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Box, Button, Typography } from '@mui/material' -import { Clock } from 'lucide-react' -import { useNavigate } from 'react-router' -import { useSessionStore } from '../../stores/sessionStore' - -export function SessionExpired() { - const { logout } = useSessionStore() - const navigate = useNavigate() - - function handleRelogin() { - logout() - navigate('/auth/login') - } - - return ( - - - - - - - - Sitzung abgelaufen - - - Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an. - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/auth/index.ts b/.claude/worktrees/agent-a82a3716/src/components/auth/index.ts deleted file mode 100644 index 9588616..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/auth/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { AccessDenied } from './AccessDenied' -export { SessionExpired } from './SessionExpired' -export { PermissionGate } from './PermissionGate' -export { RoleGuard } from './RoleGuard' -export { ProtectedRoute } from './ProtectedRoute' -export { DemoRoleSwitcher } from './DemoRoleSwitcher' -export { OrganizationSwitcher } from './OrganizationSwitcher' diff --git a/.claude/worktrees/agent-a82a3716/src/components/badges/AvailabilityBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/badges/AvailabilityBadge.tsx deleted file mode 100644 index 94386ed..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/badges/AvailabilityBadge.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Chip } from '@mui/material' -import { CheckCircle2, Clock, Sparkles, XCircle, HelpCircle } from 'lucide-react' -import type { AvailabilityStatus } from '../../domain/enums' -import { DS_COLORS } from '../../lib/ds' -import { AVAILABILITY_LABELS } from '../../lib/constants' - -interface AvailabilityBadgeProps { - status: AvailabilityStatus - size?: 'small' | 'medium' -} - -const ICONS: Record = { - AVAILABLE_NOW: CheckCircle2, - AVAILABLE_SOON: Clock, - FUTURE_SIGNAL: Sparkles, - OCCUPIED: XCircle, - UNKNOWN: HelpCircle, -} - -export function AvailabilityBadge({ status, size = 'small' }: AvailabilityBadgeProps) { - const { bg, fg } = DS_COLORS.availability[status] - const Icon = ICONS[status] - return ( - } - aria-label={`Verfügbarkeit: ${AVAILABILITY_LABELS[status] ?? status}`} - sx={{ - bgcolor: bg, - color: fg, - border: 'none', - fontWeight: 600, - fontSize: '0.7rem', - '& .MuiChip-icon': { ml: 0.5 }, - }} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/badges/ConfidenceBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/badges/ConfidenceBadge.tsx deleted file mode 100644 index 5dc9d91..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/badges/ConfidenceBadge.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Chip } from '@mui/material' -import { ShieldCheck } from 'lucide-react' -import type { ConfidenceLevel } from '../../domain/enums' -import { DS_COLORS, scoreToConfidenceLevel } from '../../lib/ds' -import { CONFIDENCE_LABELS } from '../../lib/constants' - -interface ConfidenceBadgeProps { - level?: ConfidenceLevel - score?: number - size?: 'small' | 'medium' -} - -export function ConfidenceBadge({ level, score, size = 'small' }: ConfidenceBadgeProps) { - const resolved: ConfidenceLevel = - level ?? (score !== undefined ? scoreToConfidenceLevel(score) : 'MEDIUM') - const { bg, fg } = DS_COLORS.confidence[resolved] - const scoreLabel = score !== undefined ? ` (${Math.round(score * 100)}%)` : '' - const label = `${CONFIDENCE_LABELS[resolved] ?? resolved}${scoreLabel}` - return ( - } - aria-label={`Konfidenz: ${label}`} - sx={{ - bgcolor: bg, - color: fg, - border: 'none', - fontWeight: 600, - fontSize: '0.7rem', - '& .MuiChip-icon': { ml: 0.5 }, - }} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/badges/DataQualityBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/badges/DataQualityBadge.tsx deleted file mode 100644 index 452a5d9..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/badges/DataQualityBadge.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Chip } from '@mui/material' -import { CheckCircle2, AlertTriangle, XCircle } from 'lucide-react' -import type { DataQualityLevel } from '../../domain/enums' -import { DS_COLORS, scoreToDataQualityLevel } from '../../lib/ds' -import { DATA_QUALITY_LABELS } from '../../lib/constants' - -interface DataQualityBadgeProps { - level?: DataQualityLevel - score?: number - showScore?: boolean - size?: 'small' | 'medium' -} - -const ICONS: Record = { - HIGH: CheckCircle2, - MEDIUM: AlertTriangle, - LOW: XCircle, - INCOMPLETE: XCircle, -} - -export function DataQualityBadge({ level, score, showScore = false, size = 'small' }: DataQualityBadgeProps) { - const resolved: DataQualityLevel = - level ?? (score !== undefined ? scoreToDataQualityLevel(score) : 'LOW') - const { bg, fg } = DS_COLORS.dataQuality[resolved] - const Icon = ICONS[resolved] - const scoreLabel = showScore && score !== undefined ? ` ${Math.round(score * 100)}%` : '' - const label = `${DATA_QUALITY_LABELS[resolved] ?? resolved}${scoreLabel}` - return ( - } - aria-label={`Datenqualität: ${label}`} - sx={{ - bgcolor: bg, - color: fg, - border: 'none', - fontWeight: 600, - fontSize: '0.7rem', - '& .MuiChip-icon': { ml: 0.5 }, - }} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/badges/FreshnessBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/badges/FreshnessBadge.tsx deleted file mode 100644 index b439d12..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/badges/FreshnessBadge.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Chip } from '@mui/material' -import { Zap, Clock, AlertCircle } from 'lucide-react' -import type { FreshnessStatus } from '../../domain/enums' -import { DS_COLORS } from '../../lib/ds' -import { FRESHNESS_LABELS } from '../../lib/constants' - -interface FreshnessBadgeProps { - status: FreshnessStatus - size?: 'small' | 'medium' -} - -const ICONS: Record = { - FRESH: Zap, - STALE: Clock, - OUTDATED: AlertCircle, -} - -export function FreshnessBadge({ status, size = 'small' }: FreshnessBadgeProps) { - const { bg, fg } = DS_COLORS.freshness[status] - const Icon = ICONS[status] - return ( - } - aria-label={`Datenaktualität: ${FRESHNESS_LABELS[status] ?? status}`} - sx={{ - bgcolor: bg, - color: fg, - border: 'none', - fontWeight: 600, - fontSize: '0.7rem', - '& .MuiChip-icon': { ml: 0.5 }, - }} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/badges/ResultTypeBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/badges/ResultTypeBadge.tsx deleted file mode 100644 index 230d948..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/badges/ResultTypeBadge.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Chip } from '@mui/material' -import { ShieldCheck, Globe, Sparkles } from 'lucide-react' -import type { ResultType } from '../../domain/enums' -import { DS_COLORS } from '../../lib/ds' -import { RESULT_TYPE_LABELS } from '../../lib/constants' - -interface ResultTypeBadgeProps { - type: ResultType - size?: 'small' | 'medium' -} - -const ICONS: Record = { - VERIFIED_PORTFOLIO: ShieldCheck, - EXTERNAL_MARKET: Globe, - FUTURE_AVAILABILITY: Sparkles, -} - -export function ResultTypeBadge({ type, size = 'small' }: ResultTypeBadgeProps) { - const { bg, fg } = DS_COLORS.resultType[type] - const Icon = ICONS[type] - return ( - } - aria-label={RESULT_TYPE_LABELS[type] ?? type} - sx={{ - bgcolor: bg, - color: fg, - border: 'none', - fontWeight: 600, - fontSize: '0.7rem', - '& .MuiChip-icon': { ml: 0.5 }, - }} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/badges/RiskBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/badges/RiskBadge.tsx deleted file mode 100644 index 73c0816..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/badges/RiskBadge.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Chip } from '@mui/material' -import { AlertTriangle, Shield } from 'lucide-react' -import type { RiskLevel } from '../../domain/enums' -import { DS_COLORS } from '../../lib/ds' -import { RISK_LABELS } from '../../lib/constants' - -interface RiskBadgeProps { - level: RiskLevel - size?: 'small' | 'medium' -} - -export function RiskBadge({ level, size = 'small' }: RiskBadgeProps) { - const { bg, fg } = DS_COLORS.risk[level] - const Icon = level === 'LOW' || level === 'MEDIUM' ? Shield : AlertTriangle - return ( - } - aria-label={`Risiko: ${RISK_LABELS[level] ?? level}`} - sx={{ - bgcolor: bg, - color: fg, - border: 'none', - fontWeight: 600, - fontSize: '0.7rem', - '& .MuiChip-icon': { ml: 0.5 }, - }} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/badges/index.ts b/.claude/worktrees/agent-a82a3716/src/components/badges/index.ts deleted file mode 100644 index 7f9e4cc..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/badges/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { ResultTypeBadge } from './ResultTypeBadge' -export { ConfidenceBadge } from './ConfidenceBadge' -export { RiskBadge } from './RiskBadge' -export { AvailabilityBadge } from './AvailabilityBadge' -export { FreshnessBadge } from './FreshnessBadge' -export { DataQualityBadge } from './DataQualityBadge' diff --git a/.claude/worktrees/agent-a82a3716/src/components/cards/CompactCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/cards/CompactCard.tsx deleted file mode 100644 index f2859dc..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/cards/CompactCard.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Box, Card, Typography } from '@mui/material' -import type { SxProps, Theme } from '@mui/material' -import type { ReactNode } from 'react' - -interface CompactCardProps { - title: string - meta?: string - leading?: ReactNode - trailing?: ReactNode - onClick?: () => void - selected?: boolean - sx?: SxProps -} - -export function CompactCard({ title, meta, leading, trailing, onClick, selected = false, sx }: CompactCardProps) { - return ( - - - {leading && {leading}} - - - {title} - - {meta && ( - - {meta} - - )} - - {trailing && {trailing}} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/cards/DecisionCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/cards/DecisionCard.tsx deleted file mode 100644 index b3c05e5..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/cards/DecisionCard.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Box, Card, CardActions, CardContent, Divider, Typography } from '@mui/material' -import type { SxProps, Theme } from '@mui/material' -import type { ReactNode } from 'react' -import { CardSkeleton } from '../ui/CardSkeleton' -import { RestrictedState } from '../ui/RestrictedState' - -interface DecisionCardProps { - title: string - subtitle?: string - badges?: ReactNode - score?: ReactNode - body?: ReactNode - actions?: ReactNode - selected?: boolean - isLoading?: boolean - isRestricted?: boolean - onClick?: () => void - sx?: SxProps -} - -export function DecisionCard({ - title, - subtitle, - badges, - score, - body, - actions, - selected = false, - isLoading = false, - isRestricted = false, - onClick, - sx, -}: DecisionCardProps) { - if (isLoading) return - - return ( - - {isRestricted ? ( - - ) : ( - <> - - - - - {title} - - {subtitle && ( - - {subtitle} - - )} - - {score && {score}} - - {badges && ( - - {badges} - - )} - - - {body && ( - <> - - {body} - - )} - - {actions && ( - <> - - {actions} - - )} - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/cards/MetricCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/cards/MetricCard.tsx deleted file mode 100644 index 461657f..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/cards/MetricCard.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Box, Card, Typography } from '@mui/material' -import { TrendingUp, TrendingDown } from 'lucide-react' -import type { SxProps, Theme } from '@mui/material' -import type { ReactNode } from 'react' - -interface MetricDelta { - value: string - positive: boolean -} - -interface MetricCardProps { - label: string - value: string | number - delta?: MetricDelta - icon?: ReactNode - color?: string - sx?: SxProps -} - -export function MetricCard({ label, value, delta, icon, color = '#1e3a5f', sx }: MetricCardProps) { - return ( - - - - - {label} - - - {value} - - {delta && ( - - {delta.positive - ? - : } - - {delta.value} - - - )} - - {icon && ( - - {icon} - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/cards/SourceTypeBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/cards/SourceTypeBadge.tsx deleted file mode 100644 index ca92967..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/cards/SourceTypeBadge.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Chip } from '@mui/material' -import { ShieldCheck, Globe, Sparkles } from 'lucide-react' -import type { ResultType } from '../../domain/enums' -import { RESULT_TYPE_LABELS } from '../../lib/constants' - -interface SourceTypeBadgeProps { - type: ResultType - size?: 'small' | 'medium' -} - -const CONFIG: Record = { - VERIFIED_PORTFOLIO: { bg: 'rgba(30,58,95,0.1)', color: '#1e3a5f', Icon: ShieldCheck }, - EXTERNAL_MARKET: { bg: 'rgba(217,119,6,0.1)', color: '#b45309', Icon: Globe }, - FUTURE_AVAILABILITY: { bg: 'rgba(124,58,237,0.1)', color: '#6d28d9', Icon: Sparkles }, -} - -export function SourceTypeBadge({ type, size = 'small' }: SourceTypeBadgeProps) { - const { bg, color, Icon } = CONFIG[type] ?? { bg: '#f1f5f9', color: '#475569', Icon: Globe } - return ( - } - label={RESULT_TYPE_LABELS[type] ?? type} - size={size} - sx={{ bgcolor: bg, color, fontWeight: 600, border: 'none', '& .MuiChip-icon': { ml: 0.5 } }} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/cards/index.ts b/.claude/worktrees/agent-a82a3716/src/components/cards/index.ts deleted file mode 100644 index 71a92a7..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/cards/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { SourceTypeBadge } from './SourceTypeBadge' -export { DecisionCard } from './DecisionCard' -export { CompactCard } from './CompactCard' -export { MetricCard } from './MetricCard' diff --git a/.claude/worktrees/agent-a82a3716/src/components/compare/AICompareSummary.tsx b/.claude/worktrees/agent-a82a3716/src/components/compare/AICompareSummary.tsx deleted file mode 100644 index 8ffcf8b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/compare/AICompareSummary.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { useState } from 'react' -import { Alert, Box, Chip, CircularProgress, Collapse, Typography } from '@mui/material' -import { ChevronDown, ChevronUp, Trophy, TrendingDown, ShieldCheck, AlertTriangle, Info, ArrowRight } from 'lucide-react' -import type { ComparisonSummary } from '../../services/aiService' - -interface Props { - summary?: ComparisonSummary - isLoading: boolean -} - -export function AICompareSummary({ summary, isLoading }: Props) { - const [open, setOpen] = useState(true) - - return ( - - setOpen(v => !v)} - sx={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - px: 2, - py: 1.25, - bgcolor: '#f8fafc', - cursor: 'pointer', - '&:hover': { bgcolor: '#f1f5f9' }, - }} - > - - AI Vergleichs-Zusammenfassung - - - {open ? : } - - - - - {isLoading && ( - - - Analyse wird erstellt… - - )} - - {!isLoading && summary && ( - - {/* Strongest option */} - - - - Stärkstes Match - {summary.strongestOption.label} - {summary.strongestOption.reason} - - - - {/* Best value */} - {summary.bestValue && ( - - - - Bestes Preis-Leistungs-Verhältnis - {summary.bestValue.label} - {summary.bestValue.reason} - - - )} - - {/* Highest confidence */} - - - - Höchste Datenkonfidenz - - {summary.highestConfidence.label} - - ({Math.round(summary.highestConfidence.confidenceLevel * 100)}%) - - - - - - {/* Tradeoffs */} - {summary.biggestTradeoffs.length > 0 && ( - - - - Wichtigste Abwägungen - {summary.biggestTradeoffs.map((t, i) => ( - · {t} - ))} - - - )} - - {/* Missing data */} - {summary.missingDataWarnings.length > 0 && ( - - - - Fehlende Informationen - {summary.missingDataWarnings.map((w, i) => ( - · {w} - ))} - - - )} - - {/* Next step */} - - - - Empfohlener nächster Schritt - {summary.recommendedNextStep} - - - - )} - - {!isLoading && !summary && ( - - Mindestens 2 Ergebnisse auswählen, um die Zusammenfassung zu generieren. - - )} - - - Diese Zusammenfassung basiert ausschliesslich auf den vorliegenden Daten und trifft keine endgültige Entscheidung. - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/compare/CompareCell.tsx b/.claude/worktrees/agent-a82a3716/src/components/compare/CompareCell.tsx deleted file mode 100644 index 6272b98..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/compare/CompareCell.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import type { ReactNode } from 'react' -import { Box, Tooltip, Typography } from '@mui/material' -import { Info } from 'lucide-react' - -export type CellHighlight = 'best' | 'worst' | 'critical' | 'future' | 'none' - -interface Props { - highlight?: CellHighlight - icon?: ReactNode - iconTooltip?: string - children: ReactNode -} - -const HIGHLIGHT_SX: Record = { - best: { bgcolor: '#f0fdf4', borderLeft: '3px solid #1a7a4a' }, - worst: { bgcolor: '#fef3c7', borderLeft: '3px solid #d97706' }, - critical: { bgcolor: '#fef2f2', borderLeft: '3px solid #c0392b' }, - future: { bgcolor: '#faf5ff', borderLeft: '3px solid #7c3aed' }, - none: {}, -} - -export function CompareCell({ highlight = 'none', icon, iconTooltip, children }: Props) { - return ( - - {icon && ( - iconTooltip - ? {icon} - : {icon} - )} - {children} - - ) -} - -export function MissingDataCell({ reason }: { reason?: string }) { - return ( - - - - - - - - Nicht verfügbar - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/compare/CompareColumnHeader.tsx b/.claude/worktrees/agent-a82a3716/src/components/compare/CompareColumnHeader.tsx deleted file mode 100644 index 92685f5..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/compare/CompareColumnHeader.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { Box, Chip, IconButton, Tooltip, Typography } from '@mui/material' -import { X, AlertTriangle } from 'lucide-react' -import type { UnifiedMatchResult } from '../../domain/unifiedResult' - -const TYPE_META: Record = { - VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' }, - EXTERNAL_MARKET: { label: 'Marktinserat', color: '#d97706' }, - FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed' }, -} - -const SCORE_COLOR = (s: number) => s >= 78 ? '#1a7a4a' : s >= 52 ? '#d97706' : '#c0392b' - -interface Props { - item: UnifiedMatchResult - onRemove: () => void -} - -export function CompareColumnHeader({ item, onRemove }: Props) { - const meta = TYPE_META[item.resultType] ?? { label: item.resultType, color: '#64748b' } - const prop = item.resultType !== 'FUTURE_AVAILABILITY' ? (item as any).property : null - const sig = item.resultType === 'FUTURE_AVAILABILITY' ? (item as any).signal : null - - const title = prop?.title ?? sig?.companyName ?? sig?.locationHint ?? '–' - const subtitle = prop?.location?.city ?? sig?.locationHint ?? '–' - const availability = prop?.availabilityDate ?? (sig ? `~${sig.timeHorizonMonths} Monate` : null) - const confidence = Math.round(item.match.confidenceLevel * 100) - const source = prop?.sourceLabel ?? sig?.source?.type ?? '–' - - return ( - - - - - - - - - - {title} - - - {subtitle} - - - - - {item.matchScore} - - /100 - - - - - : undefined} - sx={{ - fontSize: 10, - bgcolor: confidence < 60 ? '#fef3c7' : '#f0fdf4', - color: confidence < 60 ? '#92400e' : '#166534', - }} - /> - - {source && source !== '–' && ( - - )} - {availability && ( - - )} - - - {item.resultType === 'FUTURE_AVAILABILITY' && sig && ( - - - Probabilistisches Signal - - - {Math.round(sig.probability * 100)}% Wahrscheinlichkeit - - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/compare/CompareEmptyState.tsx b/.claude/worktrees/agent-a82a3716/src/components/compare/CompareEmptyState.tsx deleted file mode 100644 index de4f42e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/compare/CompareEmptyState.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Box, Button, Typography } from '@mui/material' -import { Columns2 } from 'lucide-react' -import { useNavigate } from 'react-router' - -export function CompareEmptyState() { - const navigate = useNavigate() - return ( - - - - - Keine Ergebnisse zum Vergleich - - - Fügen Sie 2–4 Ergebnisse aus dem Feed, Match Detail oder Match Center zum Vergleich hinzu. - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/compare/index.ts b/.claude/worktrees/agent-a82a3716/src/components/compare/index.ts deleted file mode 100644 index bf03c37..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/compare/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { CompareEmptyState } from './CompareEmptyState' -export { CompareColumnHeader } from './CompareColumnHeader' -export { CompareCell, MissingDataCell } from './CompareCell' -export { AICompareSummary } from './AICompareSummary' diff --git a/.claude/worktrees/agent-a82a3716/src/components/data-quality/CriticalFieldWarning.tsx b/.claude/worktrees/agent-a82a3716/src/components/data-quality/CriticalFieldWarning.tsx deleted file mode 100644 index 6e3075e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/data-quality/CriticalFieldWarning.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Alert, Box, Chip, Typography } from '@mui/material' - -interface CriticalFieldWarningProps { - fields: string[] - warnings?: string[] -} - -export function CriticalFieldWarning({ fields, warnings = [] }: CriticalFieldWarningProps) { - if (fields.length === 0 && warnings.length === 0) return null - - return ( - - {fields.length > 0 && ( - - - {fields.length} Pflichtfeld{fields.length > 1 ? 'er' : ''} fehlen — Match-Qualität reduziert - - - {fields.map(f => ( - - ))} - - - )} - {warnings.map((w, i) => ( - - {w} - - ))} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/data-quality/DataQualityBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/data-quality/DataQualityBadge.tsx deleted file mode 100644 index 90c0230..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/data-quality/DataQualityBadge.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Chip, Tooltip } from '@mui/material' -import { dataQualityHex } from '../../lib/utils' -import type { DataQuality } from '../../domain/property' - -interface DataQualityBadgeProps { - quality: DataQuality - showLabel?: boolean - size?: 'small' | 'medium' -} - -export function DataQualityBadge({ quality, showLabel = false, size = 'small' }: DataQualityBadgeProps) { - const pct = Math.round(quality.score * 100) - const hex = dataQualityHex(quality.score) - const hasCritical = quality.missingCriticalFields.length > 0 - - const levelLabel = quality.qualityLevel - ? { HIGH: 'Hoch', MEDIUM: 'Mittel', LOW: 'Niedrig', INCOMPLETE: 'Unvollständig' }[quality.qualityLevel] - : null - - const tooltipText = hasCritical - ? `${quality.missingCriticalFields.length} Pflichtfeld(er) fehlen` - : `Datenqualität: ${pct}%` - - return ( - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/data-quality/DataQualityBar.tsx b/.claude/worktrees/agent-a82a3716/src/components/data-quality/DataQualityBar.tsx deleted file mode 100644 index b4bb345..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/data-quality/DataQualityBar.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Box, LinearProgress, Tooltip, Typography, Chip } from '@mui/material' -import { AlertTriangle } from 'lucide-react' -import { dataQualityColor, dataQualityHex, formatPercent } from '../../lib/utils' -import { FRESHNESS_LABELS } from '../../lib/constants' -import type { DataQuality } from '../../domain/property' - -interface DataQualityBarProps { - quality: DataQuality - compact?: boolean - showWarnings?: boolean -} - -export function DataQualityBar({ quality, compact = false, showWarnings = true }: DataQualityBarProps) { - const color = dataQualityColor(quality.score) - const hex = dataQualityHex(quality.score) - const pct = Math.round(quality.score * 100) - - const tooltipContent = ( - - - Datenqualität {formatPercent(quality.score)} - - {quality.missingCriticalFields.length > 0 && ( - - Fehlende Pflichtfelder: - {quality.missingCriticalFields.map(f => ( - • {f} - ))} - - )} - {quality.warnings.length > 0 && ( - - Warnungen: - {quality.warnings.map((w, i) => ( - • {w} - ))} - - )} - - Aktualität: {FRESHNESS_LABELS[quality.freshness]} - {quality.lastVerifiedAt ? ` · Geprüft: ${quality.lastVerifiedAt}` : ''} - - - ) - - if (compact) { - return ( - - - - - - - {pct}% - - {quality.missingCriticalFields.length > 0 && showWarnings && ( - - )} - - - ) - } - - return ( - - - - - - - - {pct}% - - - - {showWarnings && quality.missingCriticalFields.length > 0 && ( - - {quality.missingCriticalFields.slice(0, 2).map(f => ( - - ))} - {quality.missingCriticalFields.length > 2 && ( - - +{quality.missingCriticalFields.length - 2} - - )} - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/data-quality/DataQualityPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/data-quality/DataQualityPanel.tsx deleted file mode 100644 index 86e4867..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/data-quality/DataQualityPanel.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Box, Button, Divider, Paper, Typography } from '@mui/material' -import { DataQualityProgress } from './DataQualityProgress' -import { CriticalFieldWarning } from './CriticalFieldWarning' -import { MissingDataList } from './MissingDataList' -import { ProvenancePanel } from './ProvenancePanel' -import { DataQualityBadge } from './DataQualityBadge' -import { getRecommendedActions } from '../../services/dataQualityService' -import type { Property } from '../../domain/property' - -interface DataQualityPanelProps { - property: Property -} - -function SectionLabel({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) -} - -export function DataQualityPanel({ property }: DataQualityPanelProps) { - const q = property.dataQuality - const actions = getRecommendedActions(q, q.freshness) - - return ( - - {/* ── Score & Dimensions ─────────────────────────────────────────── */} - - - - Datenqualität - - - - - {q.missingCriticalFields.length === 0 ? 'Alle Pflichtfelder vorhanden' : `${q.missingCriticalFields.length} Pflichtfeld(er) fehlen`} - - - {q.missingOptionalFields.length} optionale Felder fehlen - - - - - - - {/* ── Critical warnings ──────────────────────────────────────────── */} - - - {/* ── Missing data with actions ──────────────────────────────────── */} - - Fehlende Daten & Massnahmen - - - - - - {/* ── Provenance ─────────────────────────────────────────────────── */} - - Datenherkunft & Verifikation - - - - {/* ── Action button ──────────────────────────────────────────────── */} - - - {q.missingCriticalFields.length > 0 && ( - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/data-quality/DataQualityProgress.tsx b/.claude/worktrees/agent-a82a3716/src/components/data-quality/DataQualityProgress.tsx deleted file mode 100644 index d1a97b4..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/data-quality/DataQualityProgress.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { Box, LinearProgress, Typography } from '@mui/material' -import { dataQualityHex } from '../../lib/utils' -import { FreshnessStatus } from '../../domain/enums' -import type { Property } from '../../domain/property' - -interface Dimension { - label: string - score: number - color: string -} - -function freshnessScore(f: string): number { - if (f === FreshnessStatus.FRESH) return 100 - if (f === FreshnessStatus.STALE) return 50 - return 15 -} - -function completenessScore(missingCritical: number, missingOptional: number): number { - const critPenalty = missingCritical * 15 - const optPenalty = missingOptional * 5 - return Math.max(0, 100 - critPenalty - optPenalty) -} - -const SOURCE_PROVENANCE: Record = { - ERP_IMPORT: 95, MANUAL_ENTRY: 90, PARTNER_FEED: 80, - IMMOSCOUT_SCRAPE: 65, HOMEGATE_SCRAPE: 65, NEWHOME_SCRAPE: 60, - MATCHOFFICE_SCRAPE: 60, MAISON_WORK_SCRAPE: 60, AI_SIGNAL: 40, UNKNOWN: 30, -} - -function dimColor(score: number): string { - if (score >= 80) return '#1a7a4a' - if (score >= 55) return '#d97706' - return '#c0392b' -} - -function buildDimensions(p: Property): Dimension[] { - const compScore = completenessScore( - p.dataQuality.missingCriticalFields.length, - p.dataQuality.missingOptionalFields.length, - ) - const freshScore = freshnessScore(p.dataQuality.freshness) - const confScore = Math.round(p.confidenceScore * 100) - const provScore = SOURCE_PROVENANCE[p.sourceType] ?? 50 - const lastVerified = p.dataQuality.lastVerifiedAt - const verScore = lastVerified - ? Math.max(10, 100 - Math.floor((Date.now() - new Date(lastVerified).getTime()) / (1000 * 60 * 60 * 24)) * 2) - : 10 - - return [ - { label: 'Vollständigkeit', score: compScore, color: dimColor(compScore) }, - { label: 'Aktualität', score: freshScore, color: dimColor(freshScore) }, - { label: 'Vertrauensscore', score: confScore, color: dimColor(confScore) }, - { label: 'Herkunft', score: Math.min(100, provScore), color: dimColor(provScore) }, - { label: 'Verifikation', score: Math.min(100, verScore), color: dimColor(verScore) }, - ] -} - -interface DataQualityProgressProps { - property: Property - compact?: boolean -} - -export function DataQualityProgress({ property, compact = false }: DataQualityProgressProps) { - const dims = buildDimensions(property) - const overallPct = Math.round(property.dataQuality.score * 100) - const hex = dataQualityHex(property.dataQuality.score) - - if (compact) { - return ( - - {dims.map(d => ( - - - - ))} - - ) - } - - return ( - - {/* Overall score header */} - - - {overallPct}% - - Gesamtqualität - - - {/* Dimension bars */} - - {dims.map(d => ( - - - {d.label} - {d.score}% - - - - ))} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/data-quality/FreshnessIndicator.tsx b/.claude/worktrees/agent-a82a3716/src/components/data-quality/FreshnessIndicator.tsx deleted file mode 100644 index 8c0e88d..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/data-quality/FreshnessIndicator.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Chip, Tooltip } from '@mui/material' -import { CheckCircle, Clock, AlertTriangle } from 'lucide-react' -import { FreshnessStatus } from '../../domain/enums' -import { FRESHNESS_LABELS } from '../../lib/constants' -import type { FreshnessStatus as FreshnessStatusType } from '../../domain/enums' - -interface FreshnessIndicatorProps { - freshness: FreshnessStatusType - lastUpdated?: string - size?: 'small' | 'medium' -} - -const CONFIG: Record = { - [FreshnessStatus.FRESH]: { color: '#1a7a4a', icon: CheckCircle }, - [FreshnessStatus.STALE]: { color: '#d97706', icon: Clock }, - [FreshnessStatus.OUTDATED]: { color: '#c0392b', icon: AlertTriangle }, -} - -export function FreshnessIndicator({ freshness, lastUpdated, size = 'small' }: FreshnessIndicatorProps) { - const { color, icon: Icon } = CONFIG[freshness] ?? CONFIG[FreshnessStatus.OUTDATED] - const label = FRESHNESS_LABELS[freshness] ?? freshness - - const chip = ( - } - label={label} - sx={{ - bgcolor: `${color}18`, - color, - fontWeight: 600, - fontSize: size === 'small' ? '0.7rem' : '0.8125rem', - border: `1px solid ${color}40`, - '& .MuiChip-icon': { color }, - }} - /> - ) - - if (!lastUpdated) return chip - - return ( - - {chip} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/data-quality/MissingDataList.tsx b/.claude/worktrees/agent-a82a3716/src/components/data-quality/MissingDataList.tsx deleted file mode 100644 index 0707133..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/data-quality/MissingDataList.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Box, Button, Chip, Divider, Typography } from '@mui/material' -import { AlertTriangle, Info } from 'lucide-react' -import type { RecommendedAction } from '../../services/dataQualityService' - -interface MissingDataListProps { - criticalFields: string[] - optionalFields: string[] - recommendedActions: RecommendedAction[] - onAction?: (action: RecommendedAction) => void -} - -export function MissingDataList({ - criticalFields, - optionalFields, - recommendedActions, - onAction, -}: MissingDataListProps) { - if (criticalFields.length === 0 && optionalFields.length === 0) { - return ( - - - - Alle wichtigen Felder sind vollständig. - - - ) - } - - const criticalActions = recommendedActions.filter(a => a.priority === 'HIGH') - const otherActions = recommendedActions.filter(a => a.priority !== 'HIGH') - - return ( - - {criticalFields.length > 0 && ( - - - - - Pflichtfelder ({criticalFields.length}) - - - {criticalActions.map(a => ( - - - {a.label} - {a.detail} - - {onAction && ( - - )} - - ))} - {criticalFields - .filter(f => !criticalActions.find(a => a.field === f)) - .map(f => ( - - - - )) - } - - )} - - {optionalFields.length > 0 && ( - <> - {criticalFields.length > 0 && } - - - Optionale Felder ({optionalFields.length}) - - {otherActions.map(a => ( - - - {a.label} - {a.detail} - - {onAction && ( - - )} - - ))} - {optionalFields - .filter(f => !otherActions.find(a => a.field === f)) - .map(f => ( - - )) - } - - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/data-quality/ProvenancePanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/data-quality/ProvenancePanel.tsx deleted file mode 100644 index a74b896..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/data-quality/ProvenancePanel.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { Box, Button, Chip, LinearProgress, Typography } from '@mui/material' -import { ExternalLink, Shield, ShieldAlert } from 'lucide-react' -import { FreshnessIndicator } from './FreshnessIndicator' -import type { Property } from '../../domain/property' - -interface ProvenancePanelProps { - property: Property -} - -const SOURCE_TYPE_LABELS: Record = { - ERP_IMPORT: 'ERP-Import', - MANUAL_ENTRY: 'Manuelle Eingabe', - IMMOSCOUT_SCRAPE: 'ImmoScout24', - HOMEGATE_SCRAPE: 'Homegate', - NEWHOME_SCRAPE: 'Newhome', - MATCHOFFICE_SCRAPE: 'MatchOffice', - MAISON_WORK_SCRAPE: 'Maison & Work', - AI_SIGNAL: 'KI-Signal', - PARTNER_FEED: 'Partner-Feed', - UNKNOWN: 'Unbekannt', -} - -const SOURCE_CONFIDENCE: Record = { - ERP_IMPORT: 0.95, - MANUAL_ENTRY: 0.90, - PARTNER_FEED: 0.80, - IMMOSCOUT_SCRAPE: 0.65, - HOMEGATE_SCRAPE: 0.65, - NEWHOME_SCRAPE: 0.60, - MATCHOFFICE_SCRAPE: 0.60, - MAISON_WORK_SCRAPE: 0.60, - AI_SIGNAL: 0.40, - UNKNOWN: 0.30, -} - -function getSourceConfidence(sourceType: string): number { - return SOURCE_CONFIDENCE[sourceType] ?? 0.50 -} - -function provenanceColor(conf: number): string { - if (conf >= 0.8) return '#1a7a4a' - if (conf >= 0.6) return '#d97706' - return '#c0392b' -} - -export function ProvenancePanel({ property: p }: ProvenancePanelProps) { - const sourceLabel = SOURCE_TYPE_LABELS[p.sourceType] ?? p.sourceType - const sourceConf = getSourceConfidence(p.sourceType) - const confPct = Math.round(sourceConf * 100) - const color = provenanceColor(sourceConf) - const isVerified = sourceConf >= 0.85 - const VerifyIcon = isVerified ? Shield : ShieldAlert - - return ( - - {/* Source header */} - - - {sourceLabel} - {p.sourceLabel && p.sourceLabel !== sourceLabel && ( - · {p.sourceLabel} - )} - - - - {/* Source confidence bar */} - - - Quell-Vertrauen - {confPct}% - - - - - {/* Dates */} - - - Quellaktualisierung - - {p.sourceUpdatedAt ? new Date(p.sourceUpdatedAt).toLocaleDateString('de-CH') : '—'} - - - - Letzte Verifikation - - {p.dataQuality.lastVerifiedAt ? new Date(p.dataQuality.lastVerifiedAt).toLocaleDateString('de-CH') : '—'} - - - - - {/* Freshness */} - - Aktualität: - - - - {/* External URL */} - {p.sourceUrl && ( - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/data-quality/index.ts b/.claude/worktrees/agent-a82a3716/src/components/data-quality/index.ts deleted file mode 100644 index 72eb331..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/data-quality/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { DataQualityBar } from './DataQualityBar' -export { DataQualityBadge } from './DataQualityBadge' -export { DataQualityPanel } from './DataQualityPanel' -export { DataQualityProgress } from './DataQualityProgress' -export { FreshnessIndicator } from './FreshnessIndicator' -export { CriticalFieldWarning } from './CriticalFieldWarning' -export { MissingDataList } from './MissingDataList' -export { ProvenancePanel } from './ProvenancePanel' diff --git a/.claude/worktrees/agent-a82a3716/src/components/demand/ConfidenceFieldBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/demand/ConfidenceFieldBadge.tsx deleted file mode 100644 index a89f6ab..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/demand/ConfidenceFieldBadge.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Chip } from '@mui/material' - -interface Props { - confidence: number - size?: 'small' | 'medium' -} - -function confidenceColor(c: number): string { - if (c >= 0.8) return '#1a7a4a' - if (c >= 0.6) return '#d97706' - return '#c0392b' -} - -export function ConfidenceFieldBadge({ confidence, size = 'small' }: Props) { - return ( - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/demand/CriteriaReviewPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/demand/CriteriaReviewPanel.tsx deleted file mode 100644 index 113f159..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/demand/CriteriaReviewPanel.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Box, Card, Typography, Alert, Stack, Chip } from '@mui/material' -import type { ParseNeedResult, ParsedNeedCriteria } from '../../domain/needBuilder' -import { ExtractedFieldRow } from './ExtractedFieldRow' - -interface Props { - result: ParseNeedResult - criteria: ParsedNeedCriteria - onCriteriaChange: (c: ParsedNeedCriteria) => void -} - -const ASSET_LABELS: Record = { - OFFICE: 'Büro', LOGISTICS: 'Logistik / Lager', RETAIL: 'Retail', - PRODUCTION: 'Produktion', GASTRO: 'Gastro / F&B', LIGHT_INDUSTRIAL: 'Light Industrial', - MIXED: 'Gemischt', UNKNOWN: 'Unbekannt', -} - -// ── Parse helpers ────────────────────────────────────────────────────────────── - -function parseAreaRange(s: string): ParsedNeedCriteria['areaRange'] { - const m = s.match(/(\d+)\s*[–\-]\s*(\d+)/) - if (m) return { min: parseInt(m[1]), max: parseInt(m[2]) } - const n = s.match(/(\d+)/) - if (n) { const v = parseInt(n[1]); return { min: Math.round(v * 0.8), max: Math.round(v * 1.2) } } - return undefined -} - -function parseBudget(s: string): ParsedNeedCriteria['budgetRange'] { - const n = s.match(/(\d+)/) - if (!n) return undefined - return { maxPerSqm: parseInt(n[1]), currency: 'CHF' } -} - -function parseList(s: string): string[] { - return s.split(',').map(x => x.trim()).filter(Boolean) -} - -function displayAreaRange(v: ParsedNeedCriteria['areaRange']): string { - return v ? `${v.min}–${v.max} m²` : '' -} -function displayBudget(v: ParsedNeedCriteria['budgetRange']): string { - return v ? `CHF ${v.maxPerSqm}/m²` : '' -} -function displayTiming(v: ParsedNeedCriteria['timing']): string { - if (!v) return '' - return `ab ${v.earliestMoveIn}${v.flexibleTiming ? ' (flexibel)' : ''}` -} - -// ── Section wrapper ──────────────────────────────────────────────────────────── - -function Section({ title, children }: { title: string; children: React.ReactNode }) { - return ( - - - {title} - - {children} - - ) -} - -// ── Main component ───────────────────────────────────────────────────────────── - -export function CriteriaReviewPanel({ result, criteria: c, onCriteriaChange: set }: Props) { - const { confidenceByField: conf, missingFields, assumptions } = result - - return ( - - - Extrahierte Kriterien - - - {result.rawSummary} - - - {/* ── Hard Facts ──────────────────────────────────────────────────── */} -
- set({ ...c, assetType: (v.toUpperCase() as ParsedNeedCriteria['assetType']) })} - /> - set({ ...c, areaRange: parseAreaRange(v) })} - /> - set({ ...c, preferredLocations: parseList(v) })} - /> - set({ ...c, budgetRange: parseBudget(v) })} - /> - set({ ...c, timing: { earliestMoveIn: v, latestMoveIn: v, flexibleTiming: v.toLowerCase().includes('flex') } })} - /> -
- - {/* ── Must-haves ──────────────────────────────────────────────────── */} -
- set({ ...c, mustHaveCriteria: parseList(v) })} - /> - set({ ...c, parkingNeed: v.toLowerCase().startsWith('j') })} - /> -
- - {/* ── AI Assumptions ──────────────────────────────────────────────── */} - {assumptions.length > 0 && ( -
- - {assumptions.map((a, i) => ( - - {a} - - ))} - -
- )} - - {/* ── Missing Information ─────────────────────────────────────────── */} - {missingFields.length > 0 && ( -
- - {missingFields.map(f => ( - - ))} - -
- )} -
- ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/demand/ExtractedFieldRow.tsx b/.claude/worktrees/agent-a82a3716/src/components/demand/ExtractedFieldRow.tsx deleted file mode 100644 index bfa6083..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/demand/ExtractedFieldRow.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useState } from 'react' -import { Box, IconButton, TextField, Typography } from '@mui/material' -import { Pencil } from 'lucide-react' - -interface Props { - label: string - value: string - confidence?: number - missing?: boolean - onEdit?: (value: string) => void -} - -export function ExtractedFieldRow({ label, value, missing = false, onEdit }: Props) { - const [editing, setEditing] = useState(false) - const [editValue, setEditValue] = useState(value) - - function commit() { - setEditing(false) - if (editValue !== value) onEdit?.(editValue) - } - - if (editing) { - return ( - - - {label} - - setEditValue(e.target.value)} - onBlur={commit} - onKeyDown={e => { if (e.key === 'Enter') commit() }} - /> - - ) - } - - return ( - - - - {label} - - - {value || '—'} - - - {onEdit && ( - { setEditValue(value); setEditing(true) }} - > - - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/demand/FollowUpPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/demand/FollowUpPanel.tsx deleted file mode 100644 index 0f86fb6..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/demand/FollowUpPanel.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Box, Button, Card, Typography } from '@mui/material' -import { ArrowRight, RefreshCw } from 'lucide-react' -import type { FollowUpQuestion } from '../../domain/needBuilder' -import { FollowUpQuestionCard } from './FollowUpQuestionCard' - -interface Props { - questions: FollowUpQuestion[] - answers: Record - onAnswer: (id: string, answer: string) => void - onContinue: () => void - onReparse?: () => void -} - -export function FollowUpPanel({ questions, answers, onAnswer, onContinue, onReparse }: Props) { - const requiredUnanswered = questions - .filter(q => q.importance === 'required') - .filter(q => !answers[q.id]) - - const sorted = [ - ...questions.filter(q => q.importance === 'required'), - ...questions.filter(q => q.importance === 'recommended'), - ...questions.filter(q => q.importance === 'optional'), - ] - - return ( - - - Rückfragen der KI - - - Beantworten Sie die Pflichtfelder für optimale Ergebnisse. Optionale Fragen können übersprungen werden. - - - - - {sorted.map(q => ( - onAnswer(q.id, ans)} - /> - ))} - - - - - {requiredUnanswered.length > 0 && ( - - {requiredUnanswered.length} Pflichtfeld{requiredUnanswered.length > 1 ? 'er fehlen' : ' fehlt'} noch. - - )} - {onReparse && ( - - )} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/demand/FollowUpQuestionCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/demand/FollowUpQuestionCard.tsx deleted file mode 100644 index 9b54db9..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/demand/FollowUpQuestionCard.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Box, Chip, Stack, TextField, Typography } from '@mui/material' -import type { FollowUpQuestion } from '../../domain/needBuilder' - -interface Props { - question: FollowUpQuestion - answer: string - onAnswer: (answer: string) => void -} - -const IMPORTANCE_LABEL: Record = { - required: 'Pflichtfeld', - recommended: 'Empfohlen', - optional: 'Optional', -} - -const IMPORTANCE_COLOR: Record = { - required: 'error', - recommended: 'warning', - optional: 'default', -} - -export function FollowUpQuestionCard({ question, answer, onAnswer }: Props) { - return ( - - - - {question.questionText} - - - - - {question.reason} - - - {question.suggestedAnswerOptions && question.suggestedAnswerOptions.length > 0 ? ( - - {question.suggestedAnswerOptions.map(opt => ( - onAnswer(answer === opt ? '' : opt)} - /> - ))} - - ) : ( - onAnswer(e.target.value)} - /> - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/demand/NeedBuilderErrorState.tsx b/.claude/worktrees/agent-a82a3716/src/components/demand/NeedBuilderErrorState.tsx deleted file mode 100644 index 97a8105..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/demand/NeedBuilderErrorState.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Box, Button, Typography } from '@mui/material' -import { AlertTriangle, RotateCcw } from 'lucide-react' - -interface Props { - message: string - onRetry: () => void -} - -export function NeedBuilderErrorState({ message, onRetry }: Props) { - return ( - - - - - Analyse fehlgeschlagen - - - {message} - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/demand/NeedBuilderProgress.tsx b/.claude/worktrees/agent-a82a3716/src/components/demand/NeedBuilderProgress.tsx deleted file mode 100644 index 9ea95e9..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/demand/NeedBuilderProgress.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Box, Stepper, Step, StepLabel } from '@mui/material' -import type { NeedBuilderStep } from '../../domain/needBuilder' -import { NeedBuilderStep as S } from '../../domain/needBuilder' - -interface Props { - step: NeedBuilderStep -} - -const STEPS = ['Suchkriterien & Gewichtung', 'Vorschau & Speichern'] - -function toStepIndex(step: NeedBuilderStep): number { - if ( - step === S.IDLE || - step === S.PARSING || - step === S.PARSED_REQUIRES_REVIEW || - step === S.CLARIFICATION_REQUIRED - ) return 0 - return 1 -} - -export function NeedBuilderProgress({ step }: Props) { - if (step === S.IDLE) return null - return ( - - - {STEPS.map(label => ( - - {label} - - ))} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/demand/NeedCardPreview.tsx b/.claude/worktrees/agent-a82a3716/src/components/demand/NeedCardPreview.tsx deleted file mode 100644 index 77efd59..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/demand/NeedCardPreview.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { Box, Card, Chip, Divider, LinearProgress, Stack, TextField, Typography, Alert } from '@mui/material' -import { MapPin, Ruler, Wallet, Clock, CheckSquare, ShieldAlert } from 'lucide-react' -import type { ParsedNeedCriteria } from '../../domain/needBuilder' -import { WEIGHTING_KEYS, WEIGHTING_LABELS } from '../../domain/needBuilder' -import type { WeightingKey } from '../../domain/needBuilder' - -interface Props { - criteria: ParsedNeedCriteria - weights: Record - confidenceByField: Record - missingFields: string[] - needTitle: string - onNeedTitleChange: (v: string) => void -} - -const ASSET_LABELS: Record = { - OFFICE: 'Büro', LOGISTICS: 'Logistik / Lager', RETAIL: 'Retail', - PRODUCTION: 'Produktion', GASTRO: 'Gastro / F&B', LIGHT_INDUSTRIAL: 'Light Industrial', -} - -const CRITICAL_FIELDS = ['assetType', 'areaRange', 'preferredLocations', 'budgetRange', 'timing'] - -export function NeedCardPreview({ criteria: c, weights, confidenceByField, missingFields, needTitle, onNeedTitleChange }: Props) { - const maxWeight = Math.max(...WEIGHTING_KEYS.map(k => weights[k] ?? 0), 0.01) - - const fieldEntries = Object.entries(confidenceByField) - const overallConfidence = fieldEntries.length > 0 - ? fieldEntries.reduce((sum, [, v]) => sum + v, 0) / fieldEntries.length - : 0 - const lowConfidenceFields = fieldEntries.filter(([, v]) => v < 0.6).map(([k]) => k) - const criticalMissing = missingFields.filter(f => CRITICAL_FIELDS.some(cf => f.toLowerCase().includes(cf.toLowerCase()))) - const isLowConfidence = overallConfidence < 0.6 - - return ( - - - Vorschau — Neuer Bedarf - - - {/* Need Title */} - onNeedTitleChange(e.target.value)} - sx={{ mb: 2 }} - /> - - {/* Low-confidence warning */} - {isLowConfidence && ( - } sx={{ mb: 2 }}> - Gesamtkonfidenz niedrig ({Math.round(overallConfidence * 100)}%) — Bedarf wird als Entwurf gespeichert und muss manuell geprüft werden. - - )} - - {/* Critical missing fields */} - {criticalMissing.length > 0 && ( - - Fehlende Pflichtfelder: {criticalMissing.join(', ')}. Bitte in den Kriterien ergänzen. - - )} - - - {/* Header */} - - {c.assetType && ( - - )} - - - - - {c.preferredLocations && c.preferredLocations.length > 0 && ( - - - - Standort - {c.preferredLocations.join(', ')} - - - )} - {c.areaRange && ( - - - - Fläche - {c.areaRange.min}–{c.areaRange.max} m² - - - )} - {c.budgetRange && ( - - - - Budget - max. CHF {c.budgetRange.maxPerSqm}/m² - - - )} - {c.timing && ( - - - - Verfügbarkeit - ab {c.timing.earliestMoveIn} - - - )} - - - {c.mustHaveCriteria && c.mustHaveCriteria.length > 0 && ( - - - - Must-haves - - - {c.mustHaveCriteria.map(m => ( - - ))} - - - )} - - - - {/* Confidence Summary */} - - - - Gesamtkonfidenz - - = 0.7 ? '#1a7a4a' : overallConfidence >= 0.5 ? '#d97706' : '#c0392b' }} - > - {Math.round(overallConfidence * 100)}% - - - = 0.7 ? '#1a7a4a' : overallConfidence >= 0.5 ? '#d97706' : '#c0392b', - }, - }} - /> - {lowConfidenceFields.length > 0 && ( - - Unsichere Felder: {lowConfidenceFields.join(', ')} - - )} - - - - - {/* Weights */} - - Gewichtungsprofil - - - {WEIGHTING_KEYS.map(k => { - const pct = Math.round((weights[k] ?? 0) * 100) - return ( - - {WEIGHTING_LABELS[k]} - - {pct}% - - ) - })} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/demand/NeedInput.tsx b/.claude/worktrees/agent-a82a3716/src/components/demand/NeedInput.tsx deleted file mode 100644 index 07fe2a7..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/demand/NeedInput.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { useState } from 'react' -import { Box, Card, Chip, Stack, TextField, Typography } from '@mui/material' -import type { ParsedNeedCriteria } from '../../domain/needBuilder' -import { AssetType } from '../../domain/enums' - -interface Props { - criteria: ParsedNeedCriteria - onCriteriaChange: (c: ParsedNeedCriteria) => void -} - -const ASSET_OPTIONS = [ - { label: 'Büro', value: AssetType.OFFICE }, - { label: 'Retail', value: AssetType.RETAIL }, - { label: 'Logistik', value: AssetType.LOGISTICS }, - { label: 'Produktion', value: AssetType.PRODUCTION }, - { label: 'Light Industrial', value: AssetType.LIGHT_INDUSTRIAL }, - { label: 'Gemischt', value: AssetType.MIXED }, -] - -function FieldLabel({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) -} - -export function NeedInput({ criteria: c, onCriteriaChange: set }: Props) { - const [locationDraft, setLocationDraft] = useState('') - const [mustHaveDraft, setMustHaveDraft] = useState('') - - function addLocations(raw: string) { - const tokens = raw.split(',').map(x => x.trim()).filter(Boolean) - if (!tokens.length) return - set({ ...c, preferredLocations: [...new Set([...(c.preferredLocations ?? []), ...tokens])] }) - setLocationDraft('') - } - - function addMustHaves(raw: string) { - const tokens = raw.split(',').map(x => x.trim()).filter(Boolean) - if (!tokens.length) return - set({ ...c, mustHaveCriteria: [...new Set([...(c.mustHaveCriteria ?? []), ...tokens])] }) - setMustHaveDraft('') - } - - return ( - - Kriterien verfeinern - - Ergänzen oder korrigieren Sie die extrahierten Felder. - - - {/* Asset Type */} - Nutzungstyp - - {ASSET_OPTIONS.map(opt => ( - set({ ...c, assetType: c.assetType === opt.value ? undefined : opt.value })} - sx={c.assetType === opt.value - ? { bgcolor: '#1e3a5f', color: 'white', '& .MuiChip-label': { color: 'white' } } - : {}} - /> - ))} - - - {/* Area */} - Fläche (m²) - - set({ ...c, areaRange: { min: parseInt(e.target.value) || 0, max: c.areaRange?.max ?? 0 } })} - sx={{ width: 100 }} - slotProps={{ htmlInput: { min: 0 } }} - /> - - set({ ...c, areaRange: { min: c.areaRange?.min ?? 0, max: parseInt(e.target.value) || 0 } })} - sx={{ width: 100 }} - slotProps={{ htmlInput: { min: 0 } }} - /> - - - - {/* Location */} - Standort - setLocationDraft(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter' && locationDraft.trim()) addLocations(locationDraft) }} - onBlur={() => { if (locationDraft.trim()) addLocations(locationDraft) }} - sx={{ mb: 0.75 }} - /> - {(c.preferredLocations?.length ?? 0) > 0 ? ( - - {c.preferredLocations!.map(loc => ( - set({ ...c, preferredLocations: c.preferredLocations!.filter(l => l !== loc) })} - /> - ))} - - ) : } - - {/* Budget */} - Budget (max CHF/m²) - set({ ...c, budgetRange: { maxPerSqm: parseInt(e.target.value) || 0, currency: 'CHF' } })} - sx={{ width: 160, mb: 2.5 }} - slotProps={{ htmlInput: { min: 0 } }} - /> - - {/* Timing */} - Verfügbar ab - set({ ...c, timing: { earliestMoveIn: e.target.value, latestMoveIn: e.target.value, flexibleTiming: true } })} - sx={{ width: 220, mb: 2.5 }} - /> - - {/* Must-haves */} - Must-haves - setMustHaveDraft(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter' && mustHaveDraft.trim()) addMustHaves(mustHaveDraft) }} - onBlur={() => { if (mustHaveDraft.trim()) addMustHaves(mustHaveDraft) }} - sx={{ mb: 0.75 }} - /> - {(c.mustHaveCriteria?.length ?? 0) > 0 && ( - - {c.mustHaveCriteria!.map(item => ( - set({ ...c, mustHaveCriteria: c.mustHaveCriteria!.filter(m => m !== item) })} - /> - ))} - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/demand/VoiceNeedInput.tsx b/.claude/worktrees/agent-a82a3716/src/components/demand/VoiceNeedInput.tsx deleted file mode 100644 index c0f1219..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/demand/VoiceNeedInput.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { useRef, useState } from 'react' -import { Box, Button, Card, Chip, CircularProgress, IconButton, TextField, Typography } from '@mui/material' -import { Mic, MicOff, Sparkles, X } from 'lucide-react' - -interface Props { - text: string - onTextChange: (s: string) => void - onAiSubmit: () => void - isAnalyzing: boolean - isAutoGen: boolean -} - -const EXAMPLES = [ - 'Büro 800–1000 m² Zürich-West, ab Sept. 2025, max. CHF 45/m², ÖV-Anbindung', - 'Retail-Fläche 200–400 m² Bern Innenstadt, Erdgeschoss, max. CHF 150/m², sofort', - 'Lagerhalle 2000–3000 m² Basel, Rampe, 12 m Deckenhöhe, max. CHF 15/m²', -] - -const isSpeechSupported = typeof window !== 'undefined' && - ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) - -export function VoiceNeedInput({ text, onTextChange, onAiSubmit, isAnalyzing, isAutoGen }: Props) { - const [isRecording, setIsRecording] = useState(false) - const [interimText, setInterimText] = useState('') - const recognitionRef = useRef(null) - const accumulatedRef = useRef('') - - function startRecording() { - const SpeechAPI = (window as any).SpeechRecognition ?? (window as any).webkitSpeechRecognition - if (!SpeechAPI) return - - accumulatedRef.current = text - const rec = new SpeechAPI() - rec.lang = 'de-DE' - rec.continuous = true - rec.interimResults = true - - rec.onresult = (e: any) => { - let finalPart = '' - let interimPart = '' - for (let i = e.resultIndex; i < e.results.length; i++) { - const t = e.results[i][0].transcript - if (e.results[i].isFinal) finalPart += t - else interimPart += t - } - if (finalPart) { - accumulatedRef.current = (accumulatedRef.current + ' ' + finalPart).trim() - onTextChange(accumulatedRef.current) - } - setInterimText(interimPart) - } - - rec.onend = () => { - setIsRecording(false) - setInterimText('') - if (accumulatedRef.current.length >= 15) onAiSubmit() - } - - rec.onerror = () => { setIsRecording(false); setInterimText('') } - rec.start() - recognitionRef.current = rec - setIsRecording(true) - } - - function stopRecording() { - recognitionRef.current?.stop() - } - - // Show interim text inside the field while recording - const displayValue = isRecording && interimText - ? (text + (text ? ' ' : '') + interimText) - : text - - return ( - - - - - Bedarf beschreiben - - - Schreiben oder sprechen — die KI extrahiert alle Kriterien automatisch - - - - {isRecording ? ( - - ) : isAnalyzing ? ( - - - Analysiert… - - ) : isAutoGen && text ? ( - ⚡ auto-synchronisiert - ) : null} - - - {/* Textarea + mic */} - - { - setInterimText('') - onTextChange(e.target.value) - }} - disabled={isRecording || isAnalyzing} - slotProps={{ htmlInput: { maxLength: 2000 } }} - sx={{ - '& .MuiOutlinedInput-root': { - pr: '52px', - bgcolor: isAutoGen && !isRecording ? '#f0f7ff' : 'transparent', - transition: 'background-color 0.2s', - '& textarea': { color: isRecording && interimText ? '#64748b' : 'inherit' }, - }, - }} - /> - - {isRecording ? ( - - - - ) : ( - - - - )} - - - - {/* Actions row */} - - {/* Example prompts */} - - {EXAMPLES.map((ex, i) => ( - onTextChange(ex)} - sx={{ fontSize: 10, height: 20 }} - /> - ))} - - - - {text && !isRecording && ( - onTextChange('')} sx={{ color: '#94a3b8', p: 0.5 }}> - - - )} - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/demand/WeightingEditor.tsx b/.claude/worktrees/agent-a82a3716/src/components/demand/WeightingEditor.tsx deleted file mode 100644 index 828c1eb..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/demand/WeightingEditor.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useState } from 'react' -import { Box, Button, Card, Slider, Typography } from '@mui/material' -import { RotateCcw } from 'lucide-react' -import { WEIGHTING_KEYS, WEIGHTING_LABELS } from '../../domain/needBuilder' -import type { WeightingKey } from '../../domain/needBuilder' -import { weightingService } from '../../services/weightingService' - -interface Props { - weights: Record - onChange: (weights: Record) => void - assetType?: string -} - -const IMPORTANCE_LABELS = ['', 'Unwichtig', 'Wenig wichtig', 'Wichtig', 'Sehr wichtig', 'Entscheidend'] - -function toRaw(w: Record): Record { - const max = Math.max(...WEIGHTING_KEYS.map(k => w[k] ?? 0)) - if (max === 0) return Object.fromEntries(WEIGHTING_KEYS.map(k => [k, 3])) as Record - return Object.fromEntries( - WEIGHTING_KEYS.map(k => [k, Math.max(1, Math.round(((w[k] ?? 0) / max) * 5))]) - ) as Record -} - -function rawToWeights(raw: Record): Record { - const total = WEIGHTING_KEYS.reduce((s, k) => s + (raw[k] ?? 1), 0) - return Object.fromEntries( - WEIGHTING_KEYS.map(k => [k, (raw[k] ?? 1) / total]) - ) as Record -} - -export function WeightingEditor({ weights, onChange, assetType }: Props) { - const [raw, setRaw] = useState>(() => toRaw(weights)) - - function handleSlider(key: WeightingKey, value: number) { - const updated = { ...raw, [key]: value } - setRaw(updated) - onChange(rawToWeights(updated)) - } - - function handleReset() { - const defaults = weightingService.getDefaultWeights(assetType) - setRaw(toRaw(defaults)) - onChange(defaults) - } - - return ( - - - - - Wichtigkeit der Kriterien - - - Schieber nach rechts = wichtiger. Gewichtung wird automatisch berechnet. - - - - - - - - {WEIGHTING_KEYS.map(key => { - const importance = raw[key] ?? 3 - return ( - - - - {WEIGHTING_LABELS[key]} - - - {IMPORTANCE_LABELS[importance]} - - - handleSlider(key, v as number)} - size="small" - sx={{ color: '#1e3a5f' }} - /> - - ) - })} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/demand/index.ts b/.claude/worktrees/agent-a82a3716/src/components/demand/index.ts deleted file mode 100644 index 70f64c3..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/demand/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { ConfidenceFieldBadge } from './ConfidenceFieldBadge' -export { CriteriaReviewPanel } from './CriteriaReviewPanel' -export { ExtractedFieldRow } from './ExtractedFieldRow' -export { FollowUpPanel } from './FollowUpPanel' -export { FollowUpQuestionCard } from './FollowUpQuestionCard' -export { NeedBuilderErrorState } from './NeedBuilderErrorState' -export { NeedBuilderProgress } from './NeedBuilderProgress' -export { NeedCardPreview } from './NeedCardPreview' -export { NeedInput } from './NeedInput' -export { VoiceNeedInput } from './VoiceNeedInput' -export { WeightingEditor } from './WeightingEditor' diff --git a/.claude/worktrees/agent-a82a3716/src/components/forms/FieldWithConfidence.tsx b/.claude/worktrees/agent-a82a3716/src/components/forms/FieldWithConfidence.tsx deleted file mode 100644 index 49729bb..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/forms/FieldWithConfidence.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Box, Typography } from '@mui/material' -import type { SxProps, Theme } from '@mui/material' -import type { ReactNode } from 'react' -import type { ConfidenceLevel } from '../../domain/enums' -import { ConfidenceBadge } from '../badges/ConfidenceBadge' - -interface FieldWithConfidenceProps { - label: string - value: ReactNode - confidence?: number - confidenceLevel?: ConfidenceLevel - layout?: 'row' | 'column' - sx?: SxProps -} - -export function FieldWithConfidence({ - label, - value, - confidence, - confidenceLevel, - layout = 'column', - sx, -}: FieldWithConfidenceProps) { - const showBadge = confidence !== undefined || confidenceLevel !== undefined - - if (layout === 'row') { - return ( - - - {label} - - {value} - {showBadge && ( - - )} - - ) - } - - return ( - - - - {label} - - {showBadge && ( - - )} - - {value} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/forms/PriorityChipGroup.tsx b/.claude/worktrees/agent-a82a3716/src/components/forms/PriorityChipGroup.tsx deleted file mode 100644 index adfc430..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/forms/PriorityChipGroup.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Box, Chip, Typography } from '@mui/material' -import type { SxProps, Theme } from '@mui/material' - -interface ChipOption { - value: string - label: string - color?: string -} - -interface PriorityChipGroupProps { - label?: string - options: ChipOption[] - value: string[] - onChange: (value: string[]) => void - exclusive?: boolean - sx?: SxProps -} - -export function PriorityChipGroup({ label, options, value, onChange, exclusive = false, sx }: PriorityChipGroupProps) { - function toggle(optValue: string) { - if (exclusive) { - onChange(value.includes(optValue) ? [] : [optValue]) - } else { - onChange( - value.includes(optValue) - ? value.filter(v => v !== optValue) - : [...value, optValue], - ) - } - } - - return ( - - {label && ( - - {label} - - )} - - {options.map(opt => { - const selected = value.includes(opt.value) - const accent = opt.color ?? '#1e3a5f' - return ( - toggle(opt.value)} - sx={{ - fontSize: '0.75rem', - bgcolor: selected ? accent : 'transparent', - color: selected ? '#fff' : 'text.secondary', - border: '1px solid', - borderColor: selected ? accent : 'divider', - '&:hover': { bgcolor: selected ? accent : 'rgba(0,0,0,0.04)' }, - }} - /> - ) - })} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/forms/SelectField.tsx b/.claude/worktrees/agent-a82a3716/src/components/forms/SelectField.tsx deleted file mode 100644 index 96b0da2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/forms/SelectField.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { FormControl, FormHelperText, InputLabel, MenuItem, Select } from '@mui/material' -import type { SxProps, Theme } from '@mui/material' - -interface SelectOption { - value: T - label: string -} - -interface SelectFieldProps { - label: string - value: T | '' - onChange: (value: T) => void - options: SelectOption[] - error?: string - helperText?: string - required?: boolean - disabled?: boolean - size?: 'small' | 'medium' - fullWidth?: boolean - sx?: SxProps -} - -export function SelectField({ - label, - value, - onChange, - options, - error, - helperText, - required, - disabled, - size = 'small', - fullWidth = true, - sx, -}: SelectFieldProps) { - const labelId = `select-${label.replace(/\s+/g, '-').toLowerCase()}` - return ( - - {label} - - {(error ?? helperText) && {error ?? helperText}} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/forms/TextInput.tsx b/.claude/worktrees/agent-a82a3716/src/components/forms/TextInput.tsx deleted file mode 100644 index 950c063..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/forms/TextInput.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { TextField } from '@mui/material' -import type { SxProps, Theme } from '@mui/material' - -interface TextInputProps { - label: string - value: string - onChange: (value: string) => void - error?: string - helperText?: string - placeholder?: string - required?: boolean - disabled?: boolean - multiline?: boolean - rows?: number - size?: 'small' | 'medium' - fullWidth?: boolean - sx?: SxProps -} - -export function TextInput({ - label, - value, - onChange, - error, - helperText, - placeholder, - required, - disabled, - multiline, - rows, - size = 'small', - fullWidth = true, - sx, -}: TextInputProps) { - return ( - onChange(e.target.value)} - error={!!error} - helperText={error ?? helperText} - placeholder={placeholder} - required={required} - disabled={disabled} - multiline={multiline} - rows={rows} - size={size} - fullWidth={fullWidth} - sx={sx} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/forms/index.ts b/.claude/worktrees/agent-a82a3716/src/components/forms/index.ts deleted file mode 100644 index 9757342..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/forms/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { TextInput } from './TextInput' -export { SelectField } from './SelectField' -export { PriorityChipGroup } from './PriorityChipGroup' -export { FieldWithConfidence } from './FieldWithConfidence' diff --git a/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalCard.tsx deleted file mode 100644 index e24bf62..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalCard.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Box, Chip, LinearProgress, Typography } from '@mui/material' -import { SignalTypeBadge } from './SignalTypeBadge' -import { SensitivityBadge } from './SensitivityBadge' -import { SignalReviewStatusBadge } from './SignalReviewStatusBadge' -import { FutureSignalDisclaimer } from './FutureSignalDisclaimer' -import type { FutureSignal } from '../../domain/futureSignal' - -const SOURCE_LABELS: Record = { - PRESS: 'Presse', - CONSTRUCTION_PERMIT: 'Baubewilligung', - JOB_POSTING: 'Stelleninserat', - COMPANY_REPORT: 'Geschäftsbericht', - MARKET_DATA: 'Marktdaten', - MANUAL: 'Manuell', -} - -function probColor(p: number): string { - return p >= 0.7 ? '#1a7a4a' : p >= 0.5 ? '#d97706' : '#c0392b' -} - -interface Props { - signal: FutureSignal - isSelected: boolean - onSelect: (signal: FutureSignal) => void -} - -export function FutureSignalCard({ signal, isSelected, onSelect }: Props) { - const isConfidential = signal.sensitivityLevel === 'CONFIDENTIAL' - - return ( - onSelect(signal)} - sx={{ - p: 2, - cursor: 'pointer', - borderBottom: '1px solid #f1f5f9', - borderLeft: isSelected - ? '3px solid #1e3a5f' - : isConfidential - ? '3px solid #d97706' - : '3px solid transparent', - bgcolor: isSelected ? '#eff6ff' : isConfidential ? '#fffbeb' : 'transparent', - '&:hover': { bgcolor: isSelected ? '#eff6ff' : '#f8fafc' }, - }} - > - {/* Row 1: type + sensitivity + review status */} - - - - - - - {/* Row 2: title / company / location */} - - {signal.title ?? signal.companyName ?? signal.locationHint} - - {(signal.title || signal.companyName) && ( - - {signal.locationHint} - - )} - - {/* Row 3: probability */} - - - Wahrscheinlichkeit - - {Math.round(signal.probability * 100)}% - - - - - - {/* Row 4: meta chips */} - - - {signal.areaSqmEstimate && ( - - )} - - - - {/* Footer: mini disclaimer */} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalDetailPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalDetailPanel.tsx deleted file mode 100644 index f7b3895..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalDetailPanel.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import { Box, Button, Chip, CircularProgress, Divider, IconButton, LinearProgress, Paper, Typography } from '@mui/material' -import { X } from 'lucide-react' -import { useState } from 'react' -import { SignalTypeBadge } from './SignalTypeBadge' -import { SensitivityBadge } from './SensitivityBadge' -import { SignalReviewStatusBadge } from './SignalReviewStatusBadge' -import { FutureSignalDisclaimer } from './FutureSignalDisclaimer' -import { useUpdateSignalReviewStatus } from '../../hooks/useFutureSignals' -import { useShortlistStore } from '../../stores/shortlistStore' -import { useToastStore } from '../../stores/toastStore' -import { reviewService } from '../../services/reviewService' -import { ReviewStatus } from '../../domain/enums' -import type { FutureSignal } from '../../domain/futureSignal' - -const SOURCE_LABELS: Record = { - PRESS: 'Pressebericht', - CONSTRUCTION_PERMIT: 'Baubewilligung', - JOB_POSTING: 'Stelleninserat', - COMPANY_REPORT: 'Geschäftsbericht', - MARKET_DATA: 'Marktdaten', - MANUAL: 'Manuell erfasst', -} - -const CREDIBILITY_META: Record = { - HIGH: { label: 'Hoch', color: '#1a7a4a' }, - MEDIUM: { label: 'Mittel', color: '#d97706' }, - LOW: { label: 'Niedrig', color: '#c0392b' }, -} - -function BarRow({ label, value }: { label: string; value: number }) { - const color = value >= 0.75 ? '#1a7a4a' : value >= 0.55 ? '#d97706' : '#c0392b' - return ( - - - {label} - {Math.round(value * 100)}% - - - - ) -} - -interface Props { - signal: FutureSignal - onClose: () => void -} - -export function FutureSignalDetailPanel({ signal, onClose }: Props) { - const updateStatus = useUpdateSignalReviewStatus() - const { openAddDialog } = useShortlistStore() - const showToast = useToastStore((s) => s.showToast) - const [reviewTaskSent, setReviewTaskSent] = useState(false) - - const reviewStatus = signal.reviewStatus ?? ReviewStatus.UNREVIEWED - const isRejected = reviewStatus === ReviewStatus.REJECTED - const isApproved = reviewStatus === ReviewStatus.APPROVED - - const STATUS_TOAST: Record = { - IN_REVIEW: 'Signal zur Prüfung markiert.', - APPROVED: 'Signal genehmigt.', - REJECTED: 'Signal abgelehnt.', - FLAGGED: 'Signal markiert.', - } - - async function handleStatus(status: typeof ReviewStatus[keyof typeof ReviewStatus]) { - try { - await updateStatus.mutateAsync({ id: signal.id, status }) - showToast(STATUS_TOAST[status] ?? 'Status aktualisiert.') - } catch { - showToast('Statusänderung fehlgeschlagen.', 'error') - } - } - - async function handleSendReview() { - try { - await reviewService.createReviewTask(signal.id) - setReviewTaskSent(true) - showToast('Prüfungsaufgabe erstellt.') - } catch { - showToast('Prüfungsaufgabe konnte nicht erstellt werden.', 'error') - } - } - - function handleShortlist() { - openAddDialog({ - resultId: signal.id, - resultType: 'FUTURE_AVAILABILITY', - title: signal.title ?? signal.companyName ?? signal.locationHint, - matchScore: Math.round(signal.confidenceScore * 100), - confidenceScore: signal.confidenceScore, - sourceLabel: SOURCE_LABELS[signal.source.type] ?? signal.source.type, - addedBy: 'admin@ideal-sharing.ch', - }) - } - - const credMeta = CREDIBILITY_META[signal.source.credibility] ?? { label: signal.source.credibility, color: '#64748b' } - - return ( - - {/* Header */} - - - - - {signal.title ?? signal.companyName ?? signal.locationHint} - - {(signal.title || signal.companyName) && ( - {signal.locationHint} - )} - - - - - - - - - - {signal.isVerified && ( - - )} - - - - {/* Body */} - - - - {/* Probability + confidence */} - - - - - - {/* Meta */} - - - Zeithorizont - ~{signal.timeHorizonMonths} Monate - - {signal.areaSqmEstimate && ( - - Geschätzte Fläche - ~{signal.areaSqmEstimate.toLocaleString('de-CH')} m² - - )} - {signal.companyName && ( - - Unternehmen - {signal.companyName} - - )} - {signal.riskLevel && ( - - Risikoniveau - {signal.riskLevel} - - )} - - - - - {/* Source */} - Quelle - - - Typ - {SOURCE_LABELS[signal.source.type] ?? signal.source.type} - - - Glaubwürdigkeit - - - {signal.source.publishedAt && ( - - Veröffentlicht - {signal.source.publishedAt} - - )} - - - {/* Evidence */} - {signal.evidence?.summary && ( - <> - - Evidenz - - {signal.evidence.summary} - - - )} - - {/* Market indicator */} - {signal.marketIndicator && ( - <> - - Marktindikator - {signal.marketIndicator} - - )} - - - - {/* Actions */} - Aktionen - - {reviewStatus === ReviewStatus.UNREVIEWED && ( - - )} - {!isRejected && !reviewTaskSent && ( - - )} - {(reviewStatus === ReviewStatus.IN_REVIEW || reviewStatus === ReviewStatus.FLAGGED) && !isApproved && ( - - )} - {!isRejected && ( - - )} - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalDisclaimer.tsx b/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalDisclaimer.tsx deleted file mode 100644 index d903120..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalDisclaimer.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Alert, Typography } from '@mui/material' - -interface Props { - mini?: boolean -} - -export function FutureSignalDisclaimer({ mini = false }: Props) { - if (mini) { - return ( - - Probabilistisches Signal – keine bestätigte Fläche - - ) - } - - return ( - - - Hinweis: Probabilistisches Zukunftssignal - - - Dieses Signal basiert auf AI-Analyse öffentlicher Daten. Es handelt sich um keine bestätigte verfügbare Fläche. - Bitte ausschliesslich für strategische Beobachtung und interne Prüfung verwenden — keine verbindlichen Aussagen gegenüber Dritten. - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalEmptyState.tsx b/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalEmptyState.tsx deleted file mode 100644 index 90c4615..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalEmptyState.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Box, Typography } from '@mui/material' -import { Filter, Radio } from 'lucide-react' - -type EmptyContext = 'no-signals' | 'filtered-empty' - -const META: Record = { - 'no-signals': { - icon: , - title: 'Keine Signale vorhanden', - desc: 'Es wurden noch keine Zukunftssignale erfasst.', - }, - 'filtered-empty': { - icon: , - title: 'Keine Signale für diese Filter', - desc: 'Passen Sie die Filtereinstellungen an, um Signale anzuzeigen.', - }, -} - -export function FutureSignalEmptyState({ context }: { context: EmptyContext }) { - const { icon, title, desc } = META[context] - return ( - - {icon} - {title} - {desc} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalFilterBar.tsx b/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalFilterBar.tsx deleted file mode 100644 index 5457bb1..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/future-signals/FutureSignalFilterBar.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { Box, Chip, FormControl, InputLabel, MenuItem, Select, Typography } from '@mui/material' -import { SignalType } from '../../domain/enums' -import { SIGNAL_TYPE_LABELS } from '../../lib/constants' - -export interface SignalFilterState { - signalType: string - minConfidence: number - sensitivityLevel: string - reviewStatus: string - timeHorizon: string -} - -export const DEFAULT_SIGNAL_FILTERS: SignalFilterState = { - signalType: '', - minConfidence: 0, - sensitivityLevel: '', - reviewStatus: '', - timeHorizon: '', -} - -interface Props { - filters: SignalFilterState - onChange: (f: SignalFilterState) => void - totalCount: number - filteredCount: number -} - -const SIGNAL_TYPES = Object.values(SignalType) - -export function FutureSignalFilterBar({ filters, onChange, totalCount, filteredCount }: Props) { - const set = (partial: Partial) => onChange({ ...filters, ...partial }) - - const activeCount = [ - filters.signalType !== '', - filters.minConfidence > 0, - filters.sensitivityLevel !== '', - filters.reviewStatus !== '', - filters.timeHorizon !== '', - ].filter(Boolean).length - - return ( - - {/* Signal type chips */} - - set({ signalType: '' })} - color={filters.signalType === '' ? 'primary' : 'default'} - sx={{ fontWeight: filters.signalType === '' ? 700 : 400 }} - /> - {SIGNAL_TYPES.map(t => ( - set({ signalType: filters.signalType === t ? '' : t })} - sx={{ - fontWeight: filters.signalType === t ? 700 : 400, - bgcolor: filters.signalType === t ? '#1e3a5f' : undefined, - color: filters.signalType === t ? 'white' : undefined, - }} - /> - ))} - - - {/* Selects */} - - - Konfidenz - - - - - Prüfstatus - - - - - Zeithorizont - - - - - Vertraulichkeit - - - - - {/* Result count */} - - {activeCount > 0 && ( - onChange(DEFAULT_SIGNAL_FILTERS)} - sx={{ bgcolor: '#eff6ff', color: '#1e3a5f' }} - /> - )} - - {filteredCount} / {totalCount} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/future-signals/SensitivityBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/future-signals/SensitivityBadge.tsx deleted file mode 100644 index 2ab119a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/future-signals/SensitivityBadge.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Chip } from '@mui/material' - -const SENSITIVITY_META: Record = { - PUBLIC: { label: 'Öffentlich', color: '#64748b' }, - INTERNAL: { label: 'Intern', color: '#d97706' }, - CONFIDENTIAL: { label: 'Vertraulich', color: '#c0392b' }, - RESTRICTED: { label: 'Eingeschränkt', color: '#7c3aed' }, -} - -export function SensitivityBadge({ level }: { level: string }) { - const meta = SENSITIVITY_META[level] ?? { label: level, color: '#64748b' } - return ( - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/future-signals/SignalReviewStatusBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/future-signals/SignalReviewStatusBadge.tsx deleted file mode 100644 index d325850..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/future-signals/SignalReviewStatusBadge.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Chip } from '@mui/material' -import type { ReviewStatus } from '../../domain/enums' - -const STATUS_META: Record = { - UNREVIEWED: { label: 'Ungeprüft', color: '#94a3b8' }, - IN_REVIEW: { label: 'In Prüfung', color: '#d97706' }, - APPROVED: { label: 'Genehmigt', color: '#1a7a4a' }, - REJECTED: { label: 'Abgelehnt', color: '#c0392b' }, - FLAGGED: { label: 'Markiert', color: '#ea580c' }, -} - -export function SignalReviewStatusBadge({ status }: { status?: ReviewStatus | null }) { - const key = status ?? 'UNREVIEWED' - const meta = STATUS_META[key] ?? { label: key, color: '#94a3b8' } - return ( - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/future-signals/SignalTypeBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/future-signals/SignalTypeBadge.tsx deleted file mode 100644 index 8022b01..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/future-signals/SignalTypeBadge.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Chip } from '@mui/material' -import type { SignalType } from '../../domain/enums' -import { SIGNAL_TYPE_LABELS } from '../../lib/constants' - -interface SignalTypeBadgeProps { - type: SignalType - size?: 'small' | 'medium' -} - -const COLOR_MAP: Record = { - EXPANSION: { bg: 'rgba(26,122,74,0.12)', color: '#1a7a4a' }, - POSSIBLE_MOVE_OUT: { bg: 'rgba(217,119,6,0.12)', color: '#d97706' }, - CONSTRUCTION_PROJECT: { bg: 'rgba(37,99,235,0.12)', color: '#1d4ed8' }, - RESTRUCTURING: { bg: 'rgba(234,88,12,0.12)', color: '#c2410c' }, - PROJECT_DEVELOPMENT: { bg: 'rgba(124,58,237,0.12)', color: '#6d28d9' }, - SPACE_CONSOLIDATION: { bg: 'rgba(100,116,139,0.12)',color: '#475569' }, -} - -export function SignalTypeBadge({ type, size = 'small' }: SignalTypeBadgeProps) { - const { bg, color } = COLOR_MAP[type] ?? { bg: '#f1f5f9', color: '#475569' } - return ( - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/future-signals/index.ts b/.claude/worktrees/agent-a82a3716/src/components/future-signals/index.ts deleted file mode 100644 index 2e07a41..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/future-signals/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { SignalTypeBadge } from './SignalTypeBadge' -export { FutureSignalDisclaimer } from './FutureSignalDisclaimer' -export { SensitivityBadge } from './SensitivityBadge' -export { SignalReviewStatusBadge } from './SignalReviewStatusBadge' -export { FutureSignalCard } from './FutureSignalCard' -export { FutureSignalFilterBar } from './FutureSignalFilterBar' -export { FutureSignalDetailPanel } from './FutureSignalDetailPanel' -export { FutureSignalEmptyState } from './FutureSignalEmptyState' -export type { SignalFilterState } from './FutureSignalFilterBar' -export { DEFAULT_SIGNAL_FILTERS } from './FutureSignalFilterBar' diff --git a/.claude/worktrees/agent-a82a3716/src/components/layout/AppShell.tsx b/.claude/worktrees/agent-a82a3716/src/components/layout/AppShell.tsx deleted file mode 100644 index d27e3a6..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/layout/AppShell.tsx +++ /dev/null @@ -1,553 +0,0 @@ -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, - Clock, - Radar, - ServerCog, - GitBranch, -} from 'lucide-react' -import type { LucideIcon } from 'lucide-react' -import { OrganizationContextBadge } from './OrganizationContextBadge' -import { UserMenu } from './UserMenu' -import { NotificationButton } from './NotificationButton' -import { RightContextPanel } from './RightContextPanel' -import { CompareTray } from './CompareTray' -import { GlobalAIAssistantDrawer, GlobalAIAssistantButton } from '../assistant' -import { useAssistantStore } from '../../stores/assistantStore' -import { ToastProvider } from '../ui' - -// --------------------------------------------------------------------------- -// 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: 'Verwaltung', - abbreviation: 'VW', - icon: Building2, - firstPath: '/supply/dashboard', - chipColor: '#1e3a5f', - navItems: [ - { path: '/supply/dashboard', label: 'Übersicht', icon: LayoutDashboard }, - { path: '/supply/properties', label: 'Meine Objekte', icon: Building2 }, - { path: '/supply/match-center', label: 'Eingehende Bedarfe', icon: Target }, - { path: '/supply/future-availability', label: 'Marktchancen', icon: TrendingUp }, - { path: '/supply/data-quality', label: 'Datenpflege', icon: CheckSquare }, - ], - }, - [WorkspaceType.DEMAND]: { - label: 'Suche', - abbreviation: 'SU', - icon: Search, - firstPath: '/demand/ai-search', - chipColor: '#1a7a4a', - navItems: [ - { path: '/demand/ai-search', label: 'Flächensuche', 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: 'Administration', - abbreviation: 'ADM', - 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 }, - { path: '/ops/market-intelligence', label: 'Market Intelligence', icon: Radar }, - { path: '/ops/source-monitoring', label: 'Source Monitoring', icon: ServerCog }, - { path: '/ops/signal-pipeline', label: 'Signal Pipeline', icon: GitBranch }, - { path: '/ops/activity-timeline', label: 'Aktivitäts-Timeline', icon: Clock }, - ], - }, -} - -// 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 - allowedWorkspaces: WorkspaceType[] - onWorkspaceClick: (workspace: WorkspaceType) => void - onToggle: () => void - userName: string - orgName: string -} - -function Sidebar({ - collapsed, - activeWorkspace, - allowedWorkspaces, - onWorkspaceClick, - onToggle, - userName, - orgName, -}: SidebarProps) { - const config = WORKSPACE_CONFIG[activeWorkspace] - const width = collapsed ? 60 : 240 - const visibleWorkspaces = WORKSPACE_ORDER.filter((ws) => allowedWorkspaces.includes(ws)) - - return ( - - {/* Logo area */} - - {collapsed ? ( - - PM - - ) : ( - - - Property - - - Match - - - )} - - - {/* Workspace tabs */} - - {visibleWorkspaces.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) - const openAssistant = useAssistantStore(s => s.open) - - return ( - - {/* Left side */} - - - - {pageName} - - - - {/* Right side */} - - - - - - - - ) -} - -// --------------------------------------------------------------------------- -// 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 ?? '' - const allowedWorkspaces = currentUser?.allowedWorkspaces ?? WORKSPACE_ORDER - - return ( - - - - - - - - - - - - - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/layout/CompareTray.tsx b/.claude/worktrees/agent-a82a3716/src/components/layout/CompareTray.tsx deleted file mode 100644 index 115ef5f..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/layout/CompareTray.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useEffect } from 'react' -import { useNavigate, useLocation } from 'react-router' -import { Box, Button, IconButton, Typography } from '@mui/material' -import { X } from 'lucide-react' -import { useCompareStore } from '../../stores/compareStore' -import { useLayoutStore } from '../../stores/layoutStore' - -const TYPE_DOT: Record = { - VERIFIED_PORTFOLIO: '#1e3a5f', - EXTERNAL_MARKET: '#d97706', - FUTURE_AVAILABILITY: '#7c3aed', -} - -export function CompareTray() { - const { compareItems, removeFromCompare, clearCompare } = useCompareStore() - const { setCompareTrayVisible } = useLayoutStore() - const navigate = useNavigate() - const location = useLocation() - const isDemand = location.pathname.startsWith('/demand') - - useEffect(() => { - setCompareTrayVisible(compareItems.length > 0 && isDemand) - }, [compareItems.length, setCompareTrayVisible, isDemand]) - - if (!isDemand) return null - - const getTitle = (item: (typeof compareItems)[number]) => { - if (item.resultType === 'FUTURE_AVAILABILITY') { - return (item as any).signal?.companyName ?? (item as any).signal?.locationHint ?? 'Signal' - } - return (item as any).property?.title ?? `Score ${item.matchScore}` - } - - return ( - 0 ? 'translateY(0)' : 'translateY(100%)', - transition: 'transform 0.25s ease', - }} - > - - Vergleich ({compareItems.length}/4) - - - - {compareItems.map((item) => ( - - - - {getTitle(item)} - - - {item.matchScore} - - removeFromCompare(item.matchId)} - sx={{ p: 0.25, color: 'rgba(255,255,255,0.5)', '&:hover': { color: '#fff' } }} - > - - - - ))} - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/layout/NotificationButton.tsx b/.claude/worktrees/agent-a82a3716/src/components/layout/NotificationButton.tsx deleted file mode 100644 index 92ffbed..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/layout/NotificationButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useState } from 'react' -import { Badge, IconButton, Popover, Typography } from '@mui/material' -import { Bell } from 'lucide-react' - -export function NotificationButton() { - const [anchorEl, setAnchorEl] = useState(null) - - function handleOpen(e: React.MouseEvent) { - setAnchorEl(e.currentTarget) - } - - function handleClose() { - setAnchorEl(null) - } - - return ( - <> - - - - - - - - Benachrichtigungen - - Keine neuen Benachrichtigungen - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/layout/OrganizationContextBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/layout/OrganizationContextBadge.tsx deleted file mode 100644 index 0ca238c..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/layout/OrganizationContextBadge.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Chip } from '@mui/material' -import { Building2 } from 'lucide-react' -import { useSessionStore } from '../../stores/sessionStore' - -export function OrganizationContextBadge() { - const { currentUser } = useSessionStore() - - if (!currentUser?.organizationName) return null - - return ( - } - label={currentUser.organizationName} - sx={{ fontSize: '0.7rem', height: 22, color: '#64748b', borderColor: '#e2e8f0' }} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/layout/PageHeader.tsx b/.claude/worktrees/agent-a82a3716/src/components/layout/PageHeader.tsx deleted file mode 100644 index 174f722..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/layout/PageHeader.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Box, Breadcrumbs, Typography } from '@mui/material' -import type { SxProps, Theme } from '@mui/material' -import type { ReactNode } from 'react' -import { NavLink } from 'react-router' - -interface BreadcrumbItem { - label: string - href?: string -} - -interface PageHeaderProps { - title: string - subtitle?: string - breadcrumbs?: BreadcrumbItem[] - primaryAction?: ReactNode - secondaryActions?: ReactNode - badge?: ReactNode - sx?: SxProps -} - -export function PageHeader({ - title, - subtitle, - breadcrumbs, - primaryAction, - secondaryActions, - badge, - sx, -}: PageHeaderProps) { - return ( - - {breadcrumbs && breadcrumbs.length > 0 && ( - - {breadcrumbs.map((crumb) => - crumb.href ? ( - - {crumb.label} - - ) : ( - - {crumb.label} - - ), - )} - - )} - - - - - - {title} - - {badge} - - {subtitle && ( - - {subtitle} - - )} - - - {(primaryAction ?? secondaryActions) && ( - - {secondaryActions} - {primaryAction} - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/layout/RightContextPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/layout/RightContextPanel.tsx deleted file mode 100644 index cb6d123..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/layout/RightContextPanel.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Box, Divider, IconButton, Typography } from '@mui/material' -import { X } from 'lucide-react' -import { useLayoutStore } from '../../stores/layoutStore' -import type { RightPanelContentType } from '../../stores/layoutStore' - -const PANEL_TITLES: Record = { - ai_context: 'KI Kontext', - detail_preview: 'Detail Vorschau', - compare_preview: 'Vergleich Vorschau', - activity_feed: 'Aktivitäts-Feed', -} - -const PANEL_PLACEHOLDERS: Record = { - ai_context: 'KI-Kontext wird geladen...', - detail_preview: 'Kein Objekt ausgewählt.', - compare_preview: 'Vergleichsvorschau nicht verfügbar.', - activity_feed: 'Keine Aktivitäten vorhanden.', -} - -export function RightContextPanel() { - const { isRightPanelOpen, rightPanelContentType, closeRightPanel } = useLayoutStore() - - const title = rightPanelContentType ? PANEL_TITLES[rightPanelContentType] : '' - const placeholder = rightPanelContentType ? PANEL_PLACEHOLDERS[rightPanelContentType] : '' - - return ( - - - {title} - - - - - - - - - - {placeholder} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/layout/UserMenu.tsx b/.claude/worktrees/agent-a82a3716/src/components/layout/UserMenu.tsx deleted file mode 100644 index 6e4e5c4..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/layout/UserMenu.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useState } from 'react' -import { useNavigate } from 'react-router' -import { Avatar, Box, Divider, IconButton, ListItemIcon, Menu, MenuItem, Typography } from '@mui/material' -import { LogOut, Settings, User } from 'lucide-react' -import { useSessionStore } from '../../stores/sessionStore' -import { useToastStore } from '../../stores/toastStore' -import { DemoRoleSwitcher } from '../auth/DemoRoleSwitcher' - -const ROLE_LABELS: Record = { - SUPER_ADMIN: 'Super Admin', - ORGANIZATION_ADMIN: 'Org Admin', - PROPERTY_MANAGER: 'Property Manager', - REVIEWER: 'Reviewer', - OWNER_VIEWER: 'Owner Viewer', - DEMAND_USER: 'Demand User', -} - -function getUserInitials(name: string): string { - return name - .split(' ') - .map((n) => n[0]) - .join('') - .toUpperCase() - .slice(0, 2) -} - -export function UserMenu() { - const [anchorEl, setAnchorEl] = useState(null) - const { currentUser, logout } = useSessionStore() - const navigate = useNavigate() - const showToast = useToastStore((s) => s.showToast) - - function handleOpen(e: React.MouseEvent) { - setAnchorEl(e.currentTarget) - } - - function handleClose() { - setAnchorEl(null) - } - - function handleLogout() { - handleClose() - logout() - navigate('/auth/login') - } - - const initials = getUserInitials(currentUser?.name ?? 'U') - - return ( - <> - - - {initials} - - - - - {/* User info header */} - {currentUser && ( - - {currentUser.name} - - {ROLE_LABELS[currentUser.role] ?? currentUser.role} - - - {currentUser.organizationName} - - - )} - - - - { handleClose(); showToast('Profilseite ist in Kürze verfügbar.', 'info') }}> - - Profil - - { handleClose(); showToast('Einstellungen sind in Kürze verfügbar.', 'info') }}> - - Einstellungen - - - - - {/* Demo role switcher */} - - - - - - - - - Abmelden - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/layout/index.ts b/.claude/worktrees/agent-a82a3716/src/components/layout/index.ts deleted file mode 100644 index 90cbbd0..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/layout/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { AppShell } from './AppShell' -export { PageHeader } from './PageHeader' -export { RightContextPanel } from './RightContextPanel' -export { CompareTray } from './CompareTray' -export { UserMenu } from './UserMenu' -export { NotificationButton } from './NotificationButton' -export { OrganizationContextBadge } from './OrganizationContextBadge' diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchActionToolbar.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchActionToolbar.tsx deleted file mode 100644 index d9161c2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchActionToolbar.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Box, Button } from '@mui/material' -import type { MatchCardAction } from './MatchCardViewModel' - -const MUI_VARIANT: Record = { - primary: 'contained', - secondary: 'outlined', - danger: 'outlined', -} - -interface Props { - actions: MatchCardAction[] - compact?: boolean -} - -export function MatchActionToolbar({ actions }: Props) { - if (actions.length === 0) return null - - return ( - - {actions.map(action => ( - - ))} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCard.tsx deleted file mode 100644 index 5b82e9e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCard.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { MatchCardViewModel, MatchCardVariant } from './MatchCardViewModel' -import { MatchCardCompact } from './MatchCardCompact' -import { MatchCardExpanded } from './MatchCardExpanded' -import { MatchCardReview } from './MatchCardReview' -import { MatchCardCompareMini } from './MatchCardCompareMini' -import { MatchCardSkeleton } from './MatchCardSkeleton' -import { MatchCardRestrictedState } from './MatchCardRestrictedState' - -interface Props { - viewModel: MatchCardViewModel - variant?: MatchCardVariant - isLoading?: boolean - onRemoveCompare?: () => void -} - -export function MatchCard({ viewModel, variant = 'compact', isLoading, onRemoveCompare }: Props) { - if (isLoading) { - return ( - - ) - } - - if (viewModel.isRestricted) return - - switch (variant) { - case 'expanded': - return - case 'review': - return - case 'compare-mini': - return - default: - return - } -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardCompact.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardCompact.tsx deleted file mode 100644 index 423750e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardCompact.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { Alert, Box, Card, Divider, Typography } from '@mui/material' -import { MapPin } from 'lucide-react' -import { MatchCardHeader } from './MatchCardHeader' -import { MatchReasonList } from './MatchReasonList' -import { TradeoffList } from './TradeoffList' -import { MatchDataQualitySummary } from './MatchDataQualitySummary' -import { MatchActionToolbar } from './MatchActionToolbar' -import { MatchCardRestrictedState } from './MatchCardRestrictedState' -import type { MatchCardViewModel } from './MatchCardViewModel' - -interface Props { - vm: MatchCardViewModel -} - -export function MatchCardCompact({ vm }: Props) { - if (vm.isRestricted) return - - const borderColor = vm.isCompareSelected - ? '#7c3aed' - : vm.isSelected - ? '#1e3a5f' - : 'transparent' - - return ( - - {/* FUTURE_AVAILABILITY disclaimer — mandatory, non-dismissable */} - {vm.disclaimer && ( - - {vm.disclaimer} - - )} - - {vm.isReviewRequired && ( - - Manuelle Überprüfung erforderlich - - )} - - {/* Header: score → type → confidence → availability → risk */} - - - {/* Title + location (max 2 lines) */} - - - {vm.title} - - - - - {vm.locationLabel} - - - {vm.explainabilitySummary && ( - - {vm.explainabilitySummary} - - )} - - - - - {/* Top reason only */} - {vm.reasons.length > 0 && ( - - - - )} - - {/* Top tradeoff only (compact) */} - {vm.tradeoffs.length > 0 && ( - - - - )} - - {/* Data quality — only critical warning in compact mode */} - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardCompareMini.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardCompareMini.tsx deleted file mode 100644 index 5955298..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardCompareMini.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Box, Card, Chip, Divider, IconButton, Typography } from '@mui/material' -import { X } from 'lucide-react' -import { MatchScoreDisplay } from './MatchScoreDisplay' -import type { MatchCardViewModel } from './MatchCardViewModel' - -const RESULT_TYPE_META: Record = { - VERIFIED_PORTFOLIO: { label: 'Verified', color: '#1e3a5f' }, - EXTERNAL_MARKET: { label: 'Extern', color: '#d97706' }, - FUTURE_AVAILABILITY: { label: 'Signal', color: '#7c3aed' }, -} - -interface Props { - vm: MatchCardViewModel - onRemove?: () => void -} - -export function MatchCardCompareMini({ vm, onRemove }: Props) { - const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#64748b' } - const topReason = vm.reasons[0] - - return ( - - {onRemove && ( - - - - )} - - - - - - - - {vm.title} - - - {vm.locationLabel} - - - {topReason && ( - <> - - - {topReason.explanation} - - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardExpanded.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardExpanded.tsx deleted file mode 100644 index 305ba19..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardExpanded.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { Alert, Box, Card, Divider, Typography } from '@mui/material' -import { MapPin } from 'lucide-react' -import { MatchCardHeader } from './MatchCardHeader' -import { MatchReasonList } from './MatchReasonList' -import { TradeoffList } from './TradeoffList' -import { MatchDataQualitySummary } from './MatchDataQualitySummary' -import { MatchActionToolbar } from './MatchActionToolbar' -import { MatchCardRestrictedState } from './MatchCardRestrictedState' -import type { MatchCardViewModel } from './MatchCardViewModel' - -interface Props { - vm: MatchCardViewModel -} - -export function MatchCardExpanded({ vm }: Props) { - if (vm.isRestricted) return - - return ( - - {/* FUTURE_AVAILABILITY disclaimer — mandatory */} - {vm.disclaimer && ( - - {vm.disclaimer} - - )} - - {vm.isReviewRequired && ( - - Manuelle Überprüfung erforderlich - - )} - - - - {/* Primary summary zone */} - - - {vm.title} - - - - {vm.locationLabel} - - {vm.availabilityLabel && ( - - Verfügbar: {vm.availabilityLabel} - - )} - {vm.explainabilitySummary && ( - - {vm.explainabilitySummary} - - )} - - - - - {/* Why it matches — all 3 reasons */} - {vm.reasons.length > 0 && ( - - - - )} - - {/* Tradeoffs — up to 3 */} - {vm.tradeoffs.length > 0 && ( - <> - - - - - - )} - - {/* Data quality — full view */} - - - - - - {vm.sourceLabel && ( - - Quelle: {vm.sourceLabel} - {vm.externalUrl && ( - - ↗ - - )} - - )} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardHeader.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardHeader.tsx deleted file mode 100644 index 488dff9..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardHeader.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Box, Chip } from '@mui/material' -import { MatchScoreDisplay } from './MatchScoreDisplay' -import type { MatchCardViewModel } from './MatchCardViewModel' - -const RESULT_TYPE_META: Record = { - VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' }, - EXTERNAL_MARKET: { label: 'Marktinserat', color: '#d97706' }, - FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed' }, -} - -function confidenceColor(score: number): string { - if (score >= 0.75) return '#1a7a4a' - if (score >= 0.55) return '#d97706' - return '#c0392b' -} - -interface Props { - vm: MatchCardViewModel - compact?: boolean -} - -export function MatchCardHeader({ vm, compact }: Props) { - const rt = RESULT_TYPE_META[vm.resultType] ?? { label: vm.resultType, color: '#64748b' } - const confPct = Math.round(vm.confidenceScore * 100) - - const topRisk = vm.risks.length > 0 ? vm.risks[0].level : undefined - const showRiskBadge = topRisk && topRisk !== 'LOW' - const riskLabel = topRisk === 'CRITICAL' ? 'Kritisch' : topRisk === 'HIGH' ? 'Hohes Risiko' : 'Mittleres Risiko' - const riskColor: 'error' | 'warning' = (topRisk === 'HIGH' || topRisk === 'CRITICAL') ? 'error' : 'warning' - - return ( - - {/* Score — leftmost, most prominent */} - - - {/* Badges: resultType → assetType → confidence → availability → risk */} - - - {vm.assetType && ( - - )} - - {vm.availabilityLabel && ( - - )} - {showRiskBadge && ( - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardRestrictedState.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardRestrictedState.tsx deleted file mode 100644 index 91033f2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardRestrictedState.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Box, Card, Typography } from '@mui/material' -import { Lock } from 'lucide-react' - -interface Props { - title?: string - message?: string -} - -export function MatchCardRestrictedState({ title, message }: Props) { - return ( - - - - - - {title ?? 'Zugriff eingeschränkt'} - - - {message ?? 'Sie haben keine Berechtigung, dieses Match einzusehen.'} - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardReview.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardReview.tsx deleted file mode 100644 index fe63ee4..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardReview.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { Alert, Box, Card, Chip, Divider, Typography } from '@mui/material' -import { AlertTriangle } from 'lucide-react' -import { MatchCardHeader } from './MatchCardHeader' -import { MatchReasonList } from './MatchReasonList' -import { MatchDataQualitySummary } from './MatchDataQualitySummary' -import { MatchActionToolbar } from './MatchActionToolbar' -import type { MatchCardViewModel } from './MatchCardViewModel' -import type { Risk } from '../../domain/match' - -function riskChipColor(level: Risk['level']): 'error' | 'warning' | 'success' { - if (level === 'CRITICAL' || level === 'HIGH') return 'error' - if (level === 'MEDIUM') return 'warning' - return 'success' -} - -interface Props { - vm: MatchCardViewModel -} - -export function MatchCardReview({ vm }: Props) { - return ( - - {vm.disclaimer && ( - - {vm.disclaimer} - - )} - - - - - - {vm.title} - - - {vm.locationLabel} - - {vm.explainabilitySummary && ( - - {vm.explainabilitySummary} - - )} - - - - - {/* Reasons with scores */} - {vm.reasons.length > 0 && ( - - - - )} - - {/* Risks — prominent in review context */} - {vm.risks.length > 0 && ( - - - Risiken - - - {vm.risks.map((r, i) => ( - - - - - - - - {r.description} - - {r.mitigation && ( - - → {r.mitigation} - - )} - - - ))} - - - )} - - {/* Missing data — full detail for review */} - - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardSkeleton.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardSkeleton.tsx deleted file mode 100644 index 77e3f70..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardSkeleton.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Box, Card, Skeleton } from '@mui/material' - -interface Props { - variant?: 'compact' | 'expanded' | 'compare-mini' -} - -export function MatchCardSkeleton({ variant = 'compact' }: Props) { - if (variant === 'compare-mini') { - return ( - - - - - - ) - } - - return ( - - - - - - - - - - - - {variant === 'expanded' && ( - <> - - - - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardViewModel.ts b/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardViewModel.ts deleted file mode 100644 index 9a79e15..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchCardViewModel.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { ResultType } from '../../domain/enums' -import type { TradeOff, Risk, MissingDataItem } from '../../domain/match' - -export type MatchCardVariant = 'compact' | 'expanded' | 'review' | 'compare-mini' - -export type MatchCardActionType = - | 'OPEN_DETAIL' - | 'ADD_COMPARE' - | 'SAVE_SHORTLIST' - | 'SEND_REVIEW' - | 'APPROVE' - | 'REJECT' - | 'REQUEST_DATA' - -export interface MatchCardAction { - id: string - label: string - actionType: MatchCardActionType - variant?: 'primary' | 'secondary' | 'danger' - disabled?: boolean - onClick: () => void -} - -export interface MatchCardReason { - type: 'HARD_FACT' | 'SOFT_FACTOR' | 'STRATEGIC' - label: string - explanation: string - score: number -} - -export interface MatchCardViewModel { - id: string - title: string - resultType: ResultType - assetType?: string - matchScore: number - confidenceScore: number // 0–1 - dataQualityScore: number // 0–1 - locationLabel: string - availabilityLabel?: string - sourceLabel?: string - externalUrl?: string - reasons: MatchCardReason[] // max 3: HARD_FACT, SOFT_FACTOR, STRATEGIC - tradeoffs: TradeOff[] - risks: Risk[] - missingData: MissingDataItem[] - actions: MatchCardAction[] - disclaimer?: string // required for FUTURE_AVAILABILITY - explainabilitySummary?: string - - // States - isSelected?: boolean - isCompareSelected?: boolean - isRestricted?: boolean - isReviewRequired?: boolean - isStaleData?: boolean -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchDataQualitySummary.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchDataQualitySummary.tsx deleted file mode 100644 index efb44ac..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchDataQualitySummary.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Alert, Box, LinearProgress, Typography } from '@mui/material' -import type { MissingDataItem } from '../../domain/match' - -interface Props { - dataQualityScore: number - missingData: MissingDataItem[] - compact?: boolean -} - -export function MatchDataQualitySummary({ dataQualityScore, missingData, compact }: Props) { - const pct = Math.round(dataQualityScore * 100) - const hasCritical = missingData.some(m => m.importance === 'CRITICAL') - const criticalItem = missingData.find(m => m.importance === 'CRITICAL') - const progressColor: 'success' | 'warning' | 'error' = pct >= 80 ? 'success' : pct >= 60 ? 'warning' : 'error' - - if (compact) { - if (!hasCritical) return null - return ( - - Kritische Daten fehlen: {criticalItem?.field} - - ) - } - - return ( - - - Datenqualität - - {hasCritical && ( - - Kritische Daten fehlen: {criticalItem?.field} - - )} - - - - - - {pct}% - - - {missingData.length > 0 && ( - - {missingData.length} fehlende{missingData.length > 1 ? '' : 's'} Feld{missingData.length > 1 ? 'er' : ''} - {criticalItem ? ` · kritisch: ${criticalItem.field}` : ''} - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchReasonList.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchReasonList.tsx deleted file mode 100644 index 7c6171b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchReasonList.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Box, Typography } from '@mui/material' -import { CheckCircle2 } from 'lucide-react' -import type { MatchCardReason } from './MatchCardViewModel' - -interface Props { - reasons: MatchCardReason[] - maxItems?: number -} - -export function MatchReasonList({ reasons, maxItems = 3 }: Props) { - if (reasons.length === 0) return null - const shown = reasons.slice(0, maxItems) - - return ( - - - Warum dieses Match - - - {shown.map((r, i) => ( - - - - - {r.label} - - {r.explanation && ( - - {r.explanation} - - )} - - - ))} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchScoreDisplay.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchScoreDisplay.tsx deleted file mode 100644 index a94acf9..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/MatchScoreDisplay.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Box, Typography } from '@mui/material' - -interface Props { - score: number - size?: 'sm' | 'md' | 'lg' -} - -function scoreColor(score: number): string { - if (score >= 78) return '#1a7a4a' - if (score >= 52) return '#d97706' - return '#c0392b' -} - -const FONT_SIZES: Record = { sm: '1.25rem', md: '1.75rem', lg: '2.5rem' } - -export function MatchScoreDisplay({ score, size = 'md' }: Props) { - return ( - - - {score} - - /100 - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/TradeoffList.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-card/TradeoffList.tsx deleted file mode 100644 index 6dae321..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/TradeoffList.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Box, Typography } from '@mui/material' -import { ArrowLeftRight } from 'lucide-react' -import type { TradeOff } from '../../domain/match' - -function severityColor(severity: TradeOff['severity']): string { - if (severity === 'HIGH') return '#d97706' - if (severity === 'MEDIUM') return '#92400e' - return '#64748b' -} - -interface Props { - tradeoffs: TradeOff[] - maxItems?: number - compact?: boolean -} - -export function TradeoffList({ tradeoffs, maxItems = 3, compact }: Props) { - if (tradeoffs.length === 0) return null - const shown = tradeoffs.slice(0, maxItems) - - return ( - - - Abwägungen - - - {shown.map((t, i) => ( - - - - {compact ? ( - - {t.concern} - - ) : ( - <> - - {t.criterion} - - - {t.concern} - - {t.mitigation && ( - - → {t.mitigation} - - )} - - )} - - - ))} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-card/index.ts b/.claude/worktrees/agent-a82a3716/src/components/match-card/index.ts deleted file mode 100644 index cc6f354..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-card/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -export { MatchCard } from './MatchCard' -export { MatchCardCompact } from './MatchCardCompact' -export { MatchCardExpanded } from './MatchCardExpanded' -export { MatchCardReview } from './MatchCardReview' -export { MatchCardCompareMini } from './MatchCardCompareMini' -export { MatchCardHeader } from './MatchCardHeader' -export { MatchScoreDisplay } from './MatchScoreDisplay' -export { MatchReasonList } from './MatchReasonList' -export { TradeoffList } from './TradeoffList' -export { MatchDataQualitySummary } from './MatchDataQualitySummary' -export { MatchActionToolbar } from './MatchActionToolbar' -export { MatchCardSkeleton } from './MatchCardSkeleton' -export { MatchCardRestrictedState } from './MatchCardRestrictedState' -export type { - MatchCardViewModel, - MatchCardVariant, - MatchCardAction, - MatchCardActionType, - MatchCardReason, -} from './MatchCardViewModel' diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchBriefingPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchBriefingPanel.tsx deleted file mode 100644 index 4d68f0a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchBriefingPanel.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Box, CircularProgress, Typography } from '@mui/material' -import { useNavigate } from 'react-router' -import { useMatchesByProperty, useApproveMatch } from '../../hooks/useMatches' -import { usePropertyById } from '../../hooks/useProperties' -import { useMatchCenterStore } from '../../stores/matchCenterStore' -import { useCompareStore } from '../../stores/compareStore' -import { useToastStore } from '../../stores/toastStore' -import { MatchCardExpanded } from '../match-card/MatchCardExpanded' -import { buildMatchCardViewModel } from '../../features/matching/matchCardAdapter' -import { reviewService } from '../../services/reviewService' -import { MatchCenterEmptyState } from './MatchCenterEmptyState' -import { MatchStatusBadge } from './MatchStatusBadge' -import type { MatchCardAction } from '../match-card/MatchCardViewModel' -import type { VerifiedPortfolioResult } from '../../domain/unifiedResult' - -export function MatchBriefingPanel() { - const navigate = useNavigate() - const { selectedPropertyId, selectedNeedId } = useMatchCenterStore() - const { addToCompare } = useCompareStore() - const approveMatch = useApproveMatch() - const showToast = useToastStore((s) => s.showToast) - - const { data: matches = [], isLoading: matchesLoading } = useMatchesByProperty(selectedPropertyId ?? '') - const { data: property = null, isLoading: propLoading } = usePropertyById(selectedPropertyId) - - if (!selectedPropertyId && !selectedNeedId) return - if (selectedPropertyId && !selectedNeedId) return - if (!selectedPropertyId && selectedNeedId) return - - if (matchesLoading || propLoading) { - return ( - - - - ) - } - - const match = matches.find(m => m.needId === selectedNeedId) ?? null - - if (!match || !property) return - - const result: VerifiedPortfolioResult = { - resultType: 'VERIFIED_PORTFOLIO', - matchId: match.id, - needId: match.needId, - matchScore: match.matchScore, - match, - property, - } - - const actions: MatchCardAction[] = [ - { - id: 'approve', - label: 'Genehmigen', - actionType: 'APPROVE', - variant: 'primary', - onClick: () => approveMatch.mutate(match.id, { - onSuccess: () => showToast('Match genehmigt.'), - onError: () => showToast('Genehmigung fehlgeschlagen.', 'error'), - }), - }, - { - id: 'review', - label: 'Zur Prüfung', - actionType: 'SEND_REVIEW', - variant: 'secondary', - onClick: () => { reviewService.createReviewTask(match.id) }, - }, - { - id: 'compare', - label: 'Vergleichen', - actionType: 'ADD_COMPARE', - variant: 'secondary', - onClick: () => { addToCompare(result); navigate('/demand/compare') }, - }, - { - id: 'details', - label: 'Details', - actionType: 'OPEN_DETAIL', - variant: 'secondary', - onClick: () => navigate(`/demand/results/${match.id}`), - }, - ] - - const vm = buildMatchCardViewModel(result, actions) - - return ( - - - Match-Briefing - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchCenterEmptyState.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchCenterEmptyState.tsx deleted file mode 100644 index ec28c9f..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchCenterEmptyState.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Box, Typography } from '@mui/material' -import { Building2, Users, ArrowLeftRight } from 'lucide-react' - -type EmptyContext = 'select-both' | 'no-need' | 'no-property' | 'no-match' - -const META = { - 'select-both': { - icon: , - title: 'Objekt und Bedarf wählen', - desc: 'Wählen Sie ein Objekt links und einen Bedarf rechts, um das Match-Briefing zu sehen.', - }, - 'no-need': { - icon: , - title: 'Keinen Bedarf ausgewählt', - desc: 'Wählen Sie rechts einen Bedarf aus.', - }, - 'no-property': { - icon: , - title: 'Kein Objekt ausgewählt', - desc: 'Wählen Sie links ein Objekt aus.', - }, - 'no-match': { - icon: , - title: 'Kein Match gefunden', - desc: 'Zwischen diesem Objekt und Bedarf existiert kein berechnetes Match.', - }, -} - -export function MatchCenterEmptyState({ context }: { context: EmptyContext }) { - const { icon, title, desc } = META[context] - return ( - - {icon} - {title} - {desc} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchCenterSkeleton.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchCenterSkeleton.tsx deleted file mode 100644 index 43947b6..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchCenterSkeleton.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Box, Skeleton } from '@mui/material' - -export function MatchCenterSkeleton({ count = 4 }: { count?: number }) { - return ( - <> - {Array.from({ length: count }).map((_, i) => ( - - - - - - ))} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchListCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchListCard.tsx deleted file mode 100644 index 163326e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchListCard.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { Box, Button, Chip, Typography } from '@mui/material' -import { MatchStatusBadge } from './MatchStatusBadge' -import type { Match } from '../../domain/match' -import type { Property } from '../../domain/property' -import type { Need } from '../../domain/need' - -const STRENGTH_META: Record = { - STRONG: { label: 'Stark', bg: '#f0fdf4', color: '#1a7a4a' }, - MODERATE: { label: 'Mittel', bg: '#fefce8', color: '#d97706' }, - WEAK: { label: 'Schwach', bg: '#fff1f2', color: '#c0392b' }, -} - -function scoreColor(score: number) { - if (score >= 80) return '#1a7a4a' - if (score >= 60) return '#d97706' - return '#c0392b' -} - -interface Props { - match: Match - property: Property | undefined - need: Need | undefined - onSelect: () => void - onApprove: () => void -} - -export function MatchListCard({ match, property, need, onSelect, onApprove }: Props) { - const strength = STRENGTH_META[match.matchStrength] ?? { label: match.matchStrength, bg: '#f1f5f9', color: '#64748b' } - const summary = match.explainabilitySummary ?? '' - - return ( - - {/* Score bubble */} - - - {match.matchScore} - - - - {/* Property + need */} - - - - {property?.title ?? match.propertyId} - - - - - {[property?.location.city, property?.areaSqm ? `${property.areaSqm} m²` : null].filter(Boolean).join(' · ')} - - - {need && ( - - )} - - - - - {/* Summary excerpt */} - {summary && ( - - {summary.length > 90 ? summary.slice(0, 90) + '…' : summary} - - )} - - {/* Actions */} - e.stopPropagation()}> - {match.status !== 'APPROVED' && ( - - )} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchStatusBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchStatusBadge.tsx deleted file mode 100644 index 2e5a2c2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-center/MatchStatusBadge.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Chip } from '@mui/material' -import type { MatchStatus } from '../../domain/enums' - -const STATUS_META: Record = { - PENDING_REVIEW: { label: 'Ausstehend', color: '#d97706' }, - APPROVED: { label: 'Genehmigt', color: '#1a7a4a' }, - REJECTED: { label: 'Abgelehnt', color: '#c0392b' }, - SHORTLISTED: { label: 'Shortlist', color: '#1e3a5f' }, -} - -export function MatchStatusBadge({ status }: { status?: MatchStatus }) { - if (!status) return null - const meta = STATUS_META[status] ?? { label: status, color: '#64748b' } - return ( - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-center/NeedSelectionPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-center/NeedSelectionPanel.tsx deleted file mode 100644 index c51ac1e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-center/NeedSelectionPanel.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Box, Chip, Typography } from '@mui/material' -import { useNeeds } from '../../hooks/useNeeds' -import { useMatchCenterStore } from '../../stores/matchCenterStore' -import { MatchCenterSkeleton } from './MatchCenterSkeleton' -import type { Match } from '../../domain/match' -import type { Need } from '../../domain/need' - -interface Props { matches: Match[] } - -export function NeedSelectionPanel({ matches }: Props) { - const { data: needs = [], isLoading } = useNeeds() - const { selectedNeedId, setSelectedNeed } = useMatchCenterStore() - - if (isLoading) return - - return ( - - {needs.map((need: Need) => { - const pendingCount = matches.filter(m => m.needId === need.id && m.status === 'PENDING_REVIEW').length - const isSelected = selectedNeedId === need.id - return ( - setSelectedNeed(isSelected ? null : need.id)} - sx={{ - p: 1.5, - cursor: 'pointer', - borderBottom: '1px solid #f1f5f9', - borderRight: isSelected ? '3px solid #d97706' : '3px solid transparent', - bgcolor: isSelected ? '#fef3c7' : 'transparent', - '&:hover': { bgcolor: isSelected ? '#fef3c7' : '#f8fafc' }, - }} - > - - - {need.companyName} - - {pendingCount > 0 && ( - - )} - - - {need.requiredArea.min}–{need.requiredArea.max} m² · {need.preferredLocations.slice(0, 2).join(', ')} - - - - ) - })} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-center/PropertySelectionPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-center/PropertySelectionPanel.tsx deleted file mode 100644 index 3bb13e7..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-center/PropertySelectionPanel.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Box, Chip, Typography } from '@mui/material' -import { useProperties } from '../../hooks/useProperties' -import { useMatchCenterStore } from '../../stores/matchCenterStore' -import { MatchCenterSkeleton } from './MatchCenterSkeleton' -import type { Match } from '../../domain/match' - -interface Props { matches: Match[] } - -export function PropertySelectionPanel({ matches }: Props) { - const { data: properties = [], isLoading } = useProperties() - const { selectedPropertyId, setSelectedProperty } = useMatchCenterStore() - - if (isLoading) return - - return ( - - {properties.map(prop => { - const pendingCount = matches.filter(m => m.propertyId === prop.id && m.status === 'PENDING_REVIEW').length - const isSelected = selectedPropertyId === prop.id - return ( - setSelectedProperty(isSelected ? null : prop.id)} - sx={{ - p: 1.5, - cursor: 'pointer', - borderBottom: '1px solid #f1f5f9', - borderLeft: isSelected ? '3px solid #1e3a5f' : '3px solid transparent', - bgcolor: isSelected ? '#eff6ff' : 'transparent', - '&:hover': { bgcolor: isSelected ? '#eff6ff' : '#f8fafc' }, - }} - > - - - {prop.title} - - {pendingCount > 0 && ( - - )} - - - {prop.location.city} · {prop.areaSqm} m² - - - - ) - })} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-center/index.ts b/.claude/worktrees/agent-a82a3716/src/components/match-center/index.ts deleted file mode 100644 index 499cf8d..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-center/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { MatchStatusBadge } from './MatchStatusBadge' -export { MatchCenterEmptyState } from './MatchCenterEmptyState' -export { MatchCenterSkeleton } from './MatchCenterSkeleton' -export { PropertySelectionPanel } from './PropertySelectionPanel' -export { NeedSelectionPanel } from './NeedSelectionPanel' -export { MatchBriefingPanel } from './MatchBriefingPanel' -export { MatchListCard } from './MatchListCard' diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-detail/ExecutiveSummaryPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-detail/ExecutiveSummaryPanel.tsx deleted file mode 100644 index 0be9810..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-detail/ExecutiveSummaryPanel.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Box, Paper, Typography } from '@mui/material' -import { CheckCircle2, AlertTriangle, ArrowRight, Target } from 'lucide-react' -import type { Match } from '../../domain/match' - -interface Props { - match: Match -} - -export function ExecutiveSummaryPanel({ match }: Props) { - const topReason = match.positiveFactors[0] - const topTradeoff = match.tradeoffs?.[0] - const nextStep = match.nextBestActions?.[0] - - const rows: { icon: React.ReactNode; label: string; text: string }[] = [ - { - icon: , - label: 'Gesamteignung', - text: match.explainabilitySummary || `${match.matchStrength}-Match mit ${match.matchScore} Punkten`, - }, - ...(topReason ? [{ - icon: , - label: 'Stärkster Grund', - text: topReason.explanation, - }] : []), - ...(topTradeoff ? [{ - icon: , - label: 'Hauptabwägung', - text: topTradeoff.concern, - }] : []), - ...(nextStep ? [{ - icon: , - label: 'Empfohlener nächster Schritt', - text: nextStep.description ?? nextStep.label, - }] : []), - ] - - return ( - - Executive Summary - - {rows.map((row, i) => ( - - {row.icon} - - - {row.label} - - - {row.text} - - - - ))} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-detail/FutureAvailabilityContextPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-detail/FutureAvailabilityContextPanel.tsx deleted file mode 100644 index 665bec9..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-detail/FutureAvailabilityContextPanel.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { Alert, Box, Chip, Paper, Typography } from '@mui/material' -import type { Match } from '../../domain/match' -import type { FutureSignal } from '../../domain/futureSignal' - -const SIGNAL_TYPE_LABELS: Record = { - EXPANSION: 'Expansion', - POSSIBLE_MOVE_OUT: 'Möglicher Auszug', - CONSTRUCTION_PROJECT: 'Bauvorhaben', - RESTRUCTURING: 'Restrukturierung', - PROJECT_DEVELOPMENT: 'Projektentwicklung', - SPACE_CONSOLIDATION: 'Flächenkonsolidierung', -} - -const SENSITIVITY_META: Record = { - CONFIDENTIAL: { label: 'Vertraulich', color: 'error' }, - INTERNAL: { label: 'Intern', color: 'warning' }, - PUBLIC: { label: 'Öffentlich', color: 'default' }, -} - -const REVIEW_STATUS_META: Record = { - UNREVIEWED: { label: 'Ungeprüft', color: 'warning' }, - IN_REVIEW: { label: 'In Prüfung', color: 'warning' }, - APPROVED: { label: 'Genehmigt', color: 'success' }, - REJECTED: { label: 'Abgelehnt', color: 'error' }, - FLAGGED: { label: 'Markiert', color: 'warning' }, -} - -interface Props { - match: Match - signal: FutureSignal | null -} - -export function FutureAvailabilityContextPanel({ match: _match, signal }: Props) { - if (!signal) return null - - const sensitiveMeta = SENSITIVITY_META[signal.sensitivityLevel] ?? SENSITIVITY_META.PUBLIC - const reviewMeta = signal.reviewStatus ? (REVIEW_STATUS_META[signal.reviewStatus] ?? null) : null - - return ( - - Zukunftssignal – Kontext - - {/* Mandatory disclaimer */} - - {signal.disclaimer} - - - - {signal.signalType && ( - - Signaltyp - {SIGNAL_TYPE_LABELS[signal.signalType] ?? signal.signalType} - - )} - - Konfidenz - - {Math.round(signal.confidenceScore * 100)}% - - - - Wahrscheinlichkeit - - {Math.round(signal.probability * 100)}% - - - - Zeithorizont - ~{signal.timeHorizonMonths} Monate - - - Vertraulichkeit - - - {reviewMeta && ( - - Prüfstatus - - - )} - - Quellglaubwürdigkeit - {signal.source.credibility} - - - - {signal.evidence?.summary && ( - - - Evidenz - - {signal.evidence.summary} - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-detail/LocationIntelligencePanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-detail/LocationIntelligencePanel.tsx deleted file mode 100644 index 689d443..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-detail/LocationIntelligencePanel.tsx +++ /dev/null @@ -1,376 +0,0 @@ -import { Box, Chip, Divider, LinearProgress, Paper, Tooltip, Typography } from '@mui/material' -import { - Activity, Building2, HardHat, MapPin, Percent, - TrendingDown, TrendingUp, Train, Users, Zap, -} from 'lucide-react' -import { useProperties } from '../../hooks/useProperties' -import { getCityIntelligence } from '../../lib/locationIntelligence' -import type { Property } from '../../domain/property' - -// ── Helpers ─────────────────────────────────────────────────────────────────── - -function scoreColor(v: number) { - if (v >= 0.72) return '#1a7a4a' - if (v >= 0.48) return '#d97706' - return '#c0392b' -} - -function scoreLabel(v: number) { - if (v >= 0.82) return 'Sehr gut' - if (v >= 0.65) return 'Gut' - if (v >= 0.45) return 'Mittel' - return 'Schwach' -} - -// ── Sub-components ──────────────────────────────────────────────────────────── - -function SoftFactorBar({ - label, - value, - icon, - tooltip, -}: { - label: string - value: number | undefined | null - icon: React.ReactNode - tooltip?: string -}) { - if (value === undefined || value === null) return null - const color = scoreColor(value) - const row = ( - - - - {icon} - {label} - - - - - - ) - return tooltip ? {row} : row -} - -function KpiTile({ - label, - value, - sub, - color, -}: { - label: string - value: string - sub?: string - color?: string -}) { - return ( - - - {label} - - - {value} - - {sub && ( - - {sub} - - )} - - ) -} - -const NEW_PROJECTS: Record = { - 'Zürich': [ - { title: 'Ensemble Zürich-West', area: '500–2000 m²', completion: 'Q3 2026', note: 'Büroflächen im Neubauprojekt, Kreis 5' }, - { title: 'The Circle Phase II', area: '1000–5000 m²', completion: 'Q1 2027', note: 'Premium-Büros, Flughafen Zürich' }, - ], - 'Basel': [ - { title: 'Basel SBB Tower', area: '300–1500 m²', completion: 'Q4 2026', note: 'Gemischte Nutzung, zentrale Lage' }, - { title: 'Erlenmatt Ost', area: '600–2500 m²', completion: 'Q2 2027', note: 'Modernes Stadtentwicklungsareal' }, - ], - 'Zug': [ - { title: 'Zug Innovation Campus', area: '200–1000 m²', completion: 'Q1 2026', note: 'Steuerattraktiv, ÖV-optimal' }, - ], - 'Bern': [ - { title: 'Bern West Business Park', area: '400–3000 m²', completion: 'Q2 2026', note: 'Modernes Gewerbeareal Ausserholligen' }, - ], - 'Winterthur': [ - { title: 'Sulzerareal Phase 4', area: '800–4000 m²', completion: 'Q3 2027', note: 'Industrie-Loft-Flächen im Stadtentwicklungsgebiet' }, - ], -} - -// ── Main component ──────────────────────────────────────────────────────────── - -interface Props { - property: Property | null -} - -export function LocationIntelligencePanel({ property }: Props) { - const { data: allProperties = [] } = useProperties() - - if (!property) return null - - const city = property.location.city - const intel = getCityIntelligence(city) - const sf = property.softFactors - - const hasSoftFactors = sf && ( - sf.footfallScore !== undefined || - sf.taxEnvironmentScore !== undefined || - sf.commuterAccessScore !== undefined || - sf.talentAccessScore !== undefined || - sf.prestigeScore !== undefined || - sf.prestige !== undefined - ) - - // Market comparables: same type, same city, different property - const comparables = allProperties - .filter(p => p.id !== property.id && p.assetType === property.assetType && p.location.city === city) - .sort((a, b) => b.confidenceScore - a.confidenceScore) - .slice(0, 3) - - const newProjects = NEW_PROJECTS[city] ?? [] - - const rentTrendPositive = intel && intel.rentTrend12m > 0 - - return ( - - - Standort-Intelligence - - - Wirtschaftliche Faktoren und Marktkontext — über klassische Flächenangaben hinaus - - - {/* ── City KPIs ── */} - {intel && ( - <> - - - - = 115 ? '#1a7a4a' : intel.purchasingPowerIndex >= 95 ? '#d97706' : '#c0392b'} - /> - - - - {/* Rent trend interpretation */} - - - {rentTrendPositive ? : } - - - {rentTrendPositive - ? `Mietpreise in ${city} sind in den letzten 12 Monaten um ${intel.rentTrend12m}% gestiegen. Frühzeitig abschliessen kann vorteilhaft sein.` - : `Mietpreise in ${city} sind leicht rückläufig. Verhandlungsspielraum nutzen.`} - - - - {/* Tax + demand */} - - - - - Steuerindex Kanton - - - {intel.taxIndexCanton} (CH = 100) - - - {intel.taxIndexCanton <= 75 ? 'Sehr steuerattraktiv' : intel.taxIndexCanton <= 95 ? 'Günstige Steuerlast' : intel.taxIndexCanton <= 110 ? 'Durchschnittlich' : 'Hohe Steuerlast'} - - - - - - Nachfragestärke - - - - Aktive Nachfrage in {city} - - - - - {/* Industry clusters */} - - - Dominante Branchen-Cluster - - - {intel.dominantIndustryClusters.map(c => ( - - ))} - - - - {/* Infrastructure */} - {intel.plannedInfrastructure.length > 0 && ( - - - - - Geplante Infrastruktur-Projekte - - - {intel.plannedInfrastructure.map((proj, i) => ( - - - {proj.timeline} - - - {proj.project} - {proj.impact} - - - ))} - - )} - - - - )} - - {/* ── Soft Factors ── */} - {hasSoftFactors && ( - - - KI-berechnete Standortqualität - - } - tooltip="Geschätzte Personenfrequenz im Umfeld — relevant für Retail & Sichtbarkeit" - /> - } - tooltip={sf.publicTransportMinutes ? `~${sf.publicTransportMinutes} Min. zum nächsten ÖV-Hub` : 'Öffentliche Erreichbarkeit des Standorts'} - /> - } - tooltip="Verfügbarkeit qualifizierter Fachkräfte in einem 30-Min.-Radius" - /> - } - tooltip="Adress-Prestige und wahrgenommene Standortqualität" - /> - } - tooltip="Möglichkeit zur Flächen-Anpassung (Ausbau, Teilung, Erweiterung)" - /> - {sf.esgScore !== undefined && ( - } - tooltip="Umwelt-, Sozial- und Governance-Standard des Gebäudes" - /> - )} - - )} - - {/* ── Market Comparables ── */} - {comparables.length > 0 && ( - <> - - - Vergleichbare Angebote in {city} - - {comparables.map(p => ( - - - - - {p.title} - - - {p.areaSqm} m² · CHF {p.rentPricePerSqm}/m² - {p.rentPricePerSqm < property.rentPricePerSqm && ' · günstiger'} - {p.rentPricePerSqm > property.rentPricePerSqm && ' · teurer'} - - - - ))} - - )} - - {/* ── New Construction ── */} - {newProjects.length > 0 && ( - <> - - - Neubauprojekte als Alternative - - {newProjects.map((proj, i) => ( - - - - {proj.title} - - {proj.area} · Fertigstellung {proj.completion} - - {proj.note} - - - ))} - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-detail/MatchDetailHeader.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-detail/MatchDetailHeader.tsx deleted file mode 100644 index 53b9e21..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-detail/MatchDetailHeader.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Alert, Box, Button, Chip, Paper, Typography } from '@mui/material' -import { ArrowLeft, Bookmark, Columns2 } from 'lucide-react' -import { MatchScoreDisplay } from '../match-card/MatchScoreDisplay' -import type { Match } from '../../domain/match' -import type { Property } from '../../domain/property' -import type { FutureSignal } from '../../domain/futureSignal' - -const RESULT_TYPE_META: Record = { - VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' }, - EXTERNAL_MARKET: { label: 'Marktinserat', color: '#d97706' }, - FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed' }, -} - -function confColor(c: number): string { - return c >= 0.75 ? '#1a7a4a' : c >= 0.55 ? '#d97706' : '#c0392b' -} - -function dqColor(q: number): string { - return q >= 0.80 ? '#1a7a4a' : q >= 0.60 ? '#d97706' : '#c0392b' -} - -interface Props { - match: Match - property: Property | null - signal: FutureSignal | null - onBack: () => void - onCompare: () => void - onShortlist: () => void -} - -export function MatchDetailHeader({ match, property, signal, onBack, onCompare, onShortlist }: Props) { - const rt = RESULT_TYPE_META[match.resultType ?? 'VERIFIED_PORTFOLIO'] ?? { label: '–', color: '#64748b' } - const title = property?.title ?? signal?.companyName ?? signal?.locationHint ?? '–' - const isFuture = match.resultType === 'FUTURE_AVAILABILITY' - const dqScore = property?.dataQuality?.score ?? signal?.confidenceScore ?? 0.5 - const confPct = Math.round(match.confidenceLevel * 100) - const dqPct = Math.round(dqScore * 100) - const location = property?.location?.city - ? `${property.location.city}${property.location.district ? `, ${property.location.district}` : ''}` - : signal?.locationHint ?? '–' - const source = property?.sourceLabel ?? property?.sourceMeta?.sourceLabel ?? '–' - const availability = property?.availabilityDate ?? (signal?.timeHorizonMonths ? `~${signal.timeHorizonMonths} Monate` : undefined) - - return ( - - {isFuture && ( - - Probabilistisches Signal – keine bestätigte Fläche. Alle Angaben sind Schätzungen. - - )} - - - - - - {title} - {location} - - - - {property?.assetType && ( - - )} - - - {availability && } - {source !== '–' && ( - Quelle: {source} - )} - - - - - - - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-detail/MissingInformationPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-detail/MissingInformationPanel.tsx deleted file mode 100644 index e8f2a3d..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-detail/MissingInformationPanel.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Box, Button, Chip, Paper, Typography } from '@mui/material' -import { FileQuestion } from 'lucide-react' -import type { Match, MissingDataItem } from '../../domain/match' - -const IMPORTANCE_ORDER = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] -const IMPORTANCE_META: Record = { - CRITICAL: { label: 'Kritisch', color: 'error' }, - HIGH: { label: 'Wichtig', color: 'warning' }, - MEDIUM: { label: 'Optional', color: 'default' }, - LOW: { label: 'Optional', color: 'default' }, -} - -interface MissingItemRowProps { - item: MissingDataItem -} - -function MissingItemRow({ item }: MissingItemRowProps) { - const meta = IMPORTANCE_META[item.importance] ?? IMPORTANCE_META.LOW - - return ( - - - - {item.field} - - - - {item.description} - - - Auswirkung: {item.impact} - - - - - - - - ) -} - -interface Props { - match: Match -} - -export function MissingInformationPanel({ match }: Props) { - const missingData: MissingDataItem[] = match.missingData ?? [] - if (missingData.length === 0) return null - - const sorted = [...missingData].sort( - (a, b) => IMPORTANCE_ORDER.indexOf(a.importance) - IMPORTANCE_ORDER.indexOf(b.importance) - ) - - return ( - - - Fehlende Informationen - m.importance === 'CRITICAL') ? 'error' : 'warning'} - /> - - {sorted.map((item, i) => ( - - ))} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-detail/NeedAlignmentPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-detail/NeedAlignmentPanel.tsx deleted file mode 100644 index 0ba6fbc..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-detail/NeedAlignmentPanel.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { Box, Chip, Paper, Typography } from '@mui/material' -import { CheckCircle2, XCircle, Minus } from 'lucide-react' -import type { Match } from '../../domain/match' -import type { Property } from '../../domain/property' -import type { Need } from '../../domain/need' - -type FitStatus = 'MATCH' | 'NO_MATCH' | 'PARTIAL' | 'UNKNOWN' - -interface AlignmentRow { - label: string - needValue: string - resultValue: string - fit: FitStatus -} - -function fitIcon(fit: FitStatus) { - if (fit === 'MATCH') return - if (fit === 'NO_MATCH') return - if (fit === 'PARTIAL') return - return -} - -function fitColor(fit: FitStatus): string { - if (fit === 'MATCH') return '#f0fdf4' - if (fit === 'NO_MATCH') return '#fef2f2' - if (fit === 'PARTIAL') return '#fffbeb' - return '#f8fafc' -} - -function buildRows(need: Need, property: Property): AlignmentRow[] { - const rows: AlignmentRow[] = [] - - // Area - const areaSqm = property.areaSqm - const areaFit: FitStatus = areaSqm >= need.requiredArea.min && areaSqm <= need.requiredArea.max - ? 'MATCH' - : areaSqm >= need.requiredArea.min * 0.85 - ? 'PARTIAL' - : 'NO_MATCH' - rows.push({ - label: 'Fläche', - needValue: `${need.requiredArea.min}–${need.requiredArea.max} m²`, - resultValue: `${areaSqm} m²`, - fit: areaFit, - }) - - // Location - const locationFit: FitStatus = need.preferredLocations.some( - l => l.toLowerCase() === property.location.city.toLowerCase() || - l.toLowerCase() === property.location.district?.toLowerCase() - ) ? 'MATCH' : 'PARTIAL' - rows.push({ - label: 'Standort', - needValue: need.preferredLocations.join(', ') || '–', - resultValue: property.location.city, - fit: locationFit, - }) - - // Budget - const budgetFit: FitStatus = property.rentPricePerSqm <= need.budgetRange.maxPerSqm - ? 'MATCH' - : property.rentPricePerSqm <= need.budgetRange.maxPerSqm * 1.1 - ? 'PARTIAL' - : 'NO_MATCH' - rows.push({ - label: 'Budget', - needValue: `max. CHF ${need.budgetRange.maxPerSqm}/m²`, - resultValue: `CHF ${property.rentPricePerSqm}/m²`, - fit: budgetFit, - }) - - // Timing - const availDate = property.availabilityDate - const latestMoveIn = need.timing.latestMoveIn - const timingFit: FitStatus = !availDate ? 'UNKNOWN' - : availDate <= latestMoveIn ? 'MATCH' : 'PARTIAL' - rows.push({ - label: 'Verfügbarkeit', - needValue: `bis ${need.timing.latestMoveIn}`, - resultValue: availDate || 'unbekannt', - fit: timingFit, - }) - - return rows -} - -interface Props { - match: Match - need: Need | null - property: Property | null -} - -export function NeedAlignmentPanel({ match: _match, need, property }: Props) { - if (!need || !property) return null - - const rows = buildRows(need, property) - const mustHaves = need.mustHaveCriteria ?? [] - - return ( - - Need-Alignment - - Gesucht: {need.companyName} · {need.assetType} - - - {/* Comparison table */} - - - Kriterium - Gesucht - Objekt - Fit - - {rows.map((row, i) => ( - - {row.label} - {row.needValue} - {row.resultValue} - - {fitIcon(row.fit)} - - - ))} - - - {/* Must-haves */} - {mustHaves.length > 0 && ( - - - Must-have Kriterien - - - {mustHaves.map((m, i) => ( - - ))} - - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-detail/NextActionsPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-detail/NextActionsPanel.tsx deleted file mode 100644 index ecdf6d5..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-detail/NextActionsPanel.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Box, Button, Paper, Typography } from '@mui/material' -import type { Match, NextBestAction } from '../../domain/match' - -const PRIORITY_ORDER: Record = { HIGH: 0, MEDIUM: 1, LOW: 2 } - -const PRIORITY_COLOR: Record = { - HIGH: 'contained', - MEDIUM: 'outlined', - LOW: 'outlined', -} - -interface Props { - match: Match - onCompare?: () => void - onShortlist?: () => void - onReject?: () => void - onReview?: () => void -} - -export function NextActionsPanel({ match, onCompare, onShortlist, onReject, onReview }: Props) { - const engineActions: NextBestAction[] = [...(match.nextBestActions ?? [])].sort( - (a, b) => (PRIORITY_ORDER[a.priority] ?? 2) - (PRIORITY_ORDER[b.priority] ?? 2) - ) - - return ( - - Empfohlene Aktionen - - {engineActions.length > 0 && ( - - {engineActions.map((action, i) => ( - - - {action.description && ( - - {action.description} - - )} - - ))} - - )} - - {/* Standard actions */} - - {onShortlist && ( - - )} - {onCompare && ( - - )} - {onReview && ( - - )} - {onReject && ( - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-detail/PropertyOverviewPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-detail/PropertyOverviewPanel.tsx deleted file mode 100644 index 84aad96..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-detail/PropertyOverviewPanel.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Box, Chip, Paper, Typography } from '@mui/material' -import { Banknote, Calendar, MapPin, Maximize2, Tag } from 'lucide-react' -import type { Match } from '../../domain/match' -import type { Property } from '../../domain/property' -import type { FutureSignal } from '../../domain/futureSignal' - -interface FactRowProps { - icon: React.ReactNode - label: string - value: string -} - -function FactRow({ icon, label, value }: FactRowProps) { - return ( - - {icon} - {label} - {value} - - ) -} - -interface Props { - match: Match - property: Property | null - signal: FutureSignal | null -} - -export function PropertyOverviewPanel({ match, property, signal }: Props) { - if (!property && !signal) return null - - const isFuture = match.resultType === 'FUTURE_AVAILABILITY' - - const rows: FactRowProps[] = isFuture ? [ - { icon: , label: 'Standorthinweis', value: signal?.locationHint ?? '–' }, - { icon: , label: 'Flächenschätzung', value: signal?.areaSqmEstimate ? `~${signal.areaSqmEstimate} m²` : 'unbekannt' }, - { icon: , label: 'Zeithorizont', value: signal?.timeHorizonMonths ? `~${signal.timeHorizonMonths} Monate` : '–' }, - { icon: , label: 'Wahrscheinlichkeit', value: signal?.probability ? `${Math.round(signal.probability * 100)}%` : '–' }, - ] : [ - { icon: , label: 'Standort', value: property ? `${property.location.city}${property.location.district ? `, ${property.location.district}` : ''}` : '–' }, - { icon: , label: 'Nutzfläche', value: property ? `${property.areaSqm} m²` : '–' }, - { icon: , label: 'Mietpreis', value: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²` : '–' }, - { icon: , label: 'Verfügbar ab', value: property?.availabilityDate ?? '–' }, - { icon: , label: 'Objekttyp', value: property?.assetType ?? '–' }, - ] - - return ( - - - - {isFuture ? 'Signal-Übersicht' : 'Objekt-Übersicht'} - - {property?.status && ( - - )} - - {rows.map((row, i) => ( - - ))} - {property?.description && ( - - {property.description} - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-detail/RiskPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-detail/RiskPanel.tsx deleted file mode 100644 index 3f6cb73..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-detail/RiskPanel.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Box, Chip, Paper, Typography } from '@mui/material' -import { ShieldAlert } from 'lucide-react' -import type { Match } from '../../domain/match' -import type { Risk } from '../../domain/match' - -const LEVEL_ORDER = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] -const LEVEL_META: Record = { - CRITICAL: { label: 'Kritisch', color: 'error', bgcolor: '#fef2f2', border: '#fca5a5' }, - HIGH: { label: 'Hoch', color: 'error', bgcolor: '#fff7ed', border: '#fed7aa' }, - MEDIUM: { label: 'Mittel', color: 'warning', bgcolor: '#fffbeb', border: '#fde68a' }, - LOW: { label: 'Gering', color: 'success', bgcolor: '#f0fdf4', border: '#bbf7d0' }, -} - -interface Props { - match: Match -} - -export function RiskPanel({ match }: Props) { - const risks: Risk[] = match.risks ?? [] - if (risks.length === 0) return null - - const sorted = [...risks].sort( - (a, b) => LEVEL_ORDER.indexOf(a.level) - LEVEL_ORDER.indexOf(b.level) - ) - - return ( - - Risiken & Unsicherheiten - - {sorted.map((r, i) => { - const meta = LEVEL_META[r.level] ?? LEVEL_META.LOW - return ( - - - - {r.category} - - - - {r.description} - - {r.mitigation && ( - - → {r.mitigation} - - )} - - ) - })} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-detail/ScoreBreakdownPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-detail/ScoreBreakdownPanel.tsx deleted file mode 100644 index 161b0a7..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-detail/ScoreBreakdownPanel.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { Box, Divider, LinearProgress, Paper, Typography } from '@mui/material' -import type { Match } from '../../domain/match' - -interface BreakdownRowProps { - label: string - value: number - max: number - description: string - color?: 'success' | 'warning' | 'error' | 'primary' - modifier?: boolean -} - -function BreakdownRow({ label, value, max, description, color = 'primary', modifier }: BreakdownRowProps) { - const pct = Math.round((Math.abs(value) / max) * 100) - const isNegative = modifier && value < 0 - const isPositive = modifier && value > 0 - - return ( - - - {label} - - {modifier && value > 0 ? '+' : ''}{modifier ? value : `${value}/${max}`} - - - {!modifier && ( - - )} - {description} - - ) -} - -interface Props { - match: Match -} - -export function ScoreBreakdownPanel({ match }: Props) { - const sb = match.scoreBreakdown - - const hardColor: 'success' | 'warning' | 'error' = - sb.hardMatchScore >= 70 ? 'success' : sb.hardMatchScore >= 50 ? 'warning' : 'error' - const softColor: 'success' | 'warning' | 'error' = - sb.softFactorScore >= 70 ? 'success' : sb.softFactorScore >= 50 ? 'warning' : 'error' - - return ( - - Score Breakdown - - - - - - - - - - - - - Gesamt-Score - = 78 ? '#1a7a4a' : sb.totalScore >= 52 ? '#d97706' : '#c0392b', - }} - > - {sb.totalScore}/100 - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-detail/SourceProvenancePanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-detail/SourceProvenancePanel.tsx deleted file mode 100644 index 10c6718..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-detail/SourceProvenancePanel.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Box, Chip, Link, Paper, Typography } from '@mui/material' -import type { Property } from '../../domain/property' - -const FRESHNESS_META: Record = { - FRESH: { label: 'Aktuell (< 48h)', color: 'success' }, - STALE: { label: 'Veraltet (2–14d)', color: 'warning' }, - OUTDATED: { label: 'Outdated (> 14d)', color: 'error' }, -} - -interface Props { - property: Property | null -} - -export function SourceProvenancePanel({ property }: Props) { - if (!property) return null - - const freshness = property.dataQuality?.freshness - const freshnessMeta = freshness ? (FRESHNESS_META[freshness] ?? null) : null - const sourceUrl = property.sourceUrl ?? property.sourceMeta?.sourceUrl - const sourceLabel = property.sourceLabel ?? property.sourceMeta?.sourceLabel ?? property.sourceType ?? '–' - const sourceUpdatedAt = property.sourceUpdatedAt ?? property.sourceMeta?.sourceUpdatedAt - const lastVerified = property.dataQuality?.lastVerifiedAt - - const rows: { label: string; value: React.ReactNode }[] = [ - { label: 'Quelltyp', value: property.sourceType ?? '–' }, - { - label: 'Quelle', - value: sourceUrl ? ( - - {sourceLabel} - - ) : ( - {sourceLabel} - ), - }, - ...(sourceUpdatedAt ? [{ label: 'Letzte Aktualisierung', value: {sourceUpdatedAt} }] : []), - ...(lastVerified ? [{ label: 'Letzte Verifikation', value: {lastVerified} }] : []), - ...(freshnessMeta ? [{ - label: 'Aktualität', - value: , - }] : []), - ] - - const warnings = property.dataQuality?.warnings ?? [] - - return ( - - Quelle & Herkunft - - {rows.map((row, i) => ( - - - {row.label} - - {row.value} - - ))} - - {warnings.length > 0 && ( - - {warnings.map((w, i) => ( - - ⚠ {w} - - ))} - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-detail/TradeoffPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/match-detail/TradeoffPanel.tsx deleted file mode 100644 index a69d23f..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-detail/TradeoffPanel.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Box, Chip, Paper, Typography } from '@mui/material' -import { ArrowLeftRight } from 'lucide-react' -import type { Match } from '../../domain/match' - -const SEVERITY_META: Record = { - HIGH: { label: 'Hoch', color: 'error' }, - MEDIUM: { label: 'Mittel', color: 'warning' }, - LOW: { label: 'Gering', color: 'default' }, -} - -interface Props { - match: Match -} - -export function TradeoffPanel({ match }: Props) { - const tradeoffs = match.tradeoffs ?? [] - if (tradeoffs.length === 0) return null - - return ( - - Abwägungen - - {tradeoffs.map((t, i) => { - const meta = SEVERITY_META[t.severity] ?? SEVERITY_META.LOW - return ( - - - - - {t.criterion} - - - - - {t.concern} - - {t.mitigation && ( - - Mitigation: {t.mitigation} - - )} - {t.impactOnScore !== undefined && ( - - Score-Einfluss: {t.impactOnScore} Punkte - - )} - - ) - })} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/match-detail/index.ts b/.claude/worktrees/agent-a82a3716/src/components/match-detail/index.ts deleted file mode 100644 index 0e45ca6..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/match-detail/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { LocationIntelligencePanel } from './LocationIntelligencePanel' -export { MatchDetailHeader } from './MatchDetailHeader' -export { ExecutiveSummaryPanel } from './ExecutiveSummaryPanel' -export { PropertyOverviewPanel } from './PropertyOverviewPanel' -export { NeedAlignmentPanel } from './NeedAlignmentPanel' -export { ScoreBreakdownPanel } from './ScoreBreakdownPanel' -export { TradeoffPanel } from './TradeoffPanel' -export { RiskPanel } from './RiskPanel' -export { MissingInformationPanel } from './MissingInformationPanel' -export { SourceProvenancePanel } from './SourceProvenancePanel' -export { FutureAvailabilityContextPanel } from './FutureAvailabilityContextPanel' -export { NextActionsPanel } from './NextActionsPanel' diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/ConfidenceGatePanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/ConfidenceGatePanel.tsx deleted file mode 100644 index 8d1b32b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/ConfidenceGatePanel.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { Box, Chip, Divider, Paper, Typography } from '@mui/material' -import { CheckCircle2, ChevronRight, Target, XCircle } from 'lucide-react' -import type { GateEvaluation } from '../../domain/signalPipeline' -import { GateStatus, GATE_STATUS_LABELS, GATE_STATUS_COLORS } from '../../domain/signalPipeline' - -interface ConfidenceGatePanelProps { - gate: GateEvaluation -} - -function ConfidenceBar({ value }: { value: string | undefined }) { - if (!value) return null - - const score = parseFloat(value) - if (isNaN(score)) return null - - const pct = Math.round(score * 100) - - const barColor = - score >= 0.75 ? '#15803d' - : score >= 0.35 ? '#d97706' - : '#dc2626' - - const threshold = - score >= 0.75 ? 'Hoch (≥ 75%)' - : score >= 0.35 ? 'Mittel (≥ 35%)' - : 'Niedrig (< 35%)' - - return ( - - - - Konfidenz-Schwelle: {threshold} - - - {pct}% - - - - - - - - 0% - - - 35% - - - 75% - - - 100% - - - - ) -} - -export function ConfidenceGatePanel({ gate }: ConfidenceGatePanelProps) { - const { bg, fg } = GATE_STATUS_COLORS[gate.status] - - const confidenceCheck = gate.checks.find(c => c.label.startsWith('Konfidenz')) - const confidenceValue = confidenceCheck?.value - - return ( - - - - - Konfidenz-Gate - - - - - - - - {gate.checks.map((check, i) => ( - - {check.passed - ? - : - } - - {check.label} - {check.value && ( - - {check.value} - - )} - {check.note && ( - - ({check.note}) - - )} - - - ))} - - {gate.reason && ( - - {gate.reason} - - )} - {gate.nextAction && gate.status !== GateStatus.PASSED && ( - - - - {gate.nextAction} - - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/ConnectorRunDetailDrawer.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/ConnectorRunDetailDrawer.tsx deleted file mode 100644 index 8bb3635..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/ConnectorRunDetailDrawer.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { Box, Chip, Divider, Drawer, IconButton, Typography } from '@mui/material' -import { X, AlertCircle, AlertTriangle, Zap } from 'lucide-react' -import type { ConnectorRun } from '../../domain/dataSource' -import { CONNECTOR_RUN_STATUS_LABELS, CONNECTOR_RUN_STATUS_COLORS } from '../../domain/dataSource' - -function formatDate(iso: string): string { - return new Date(iso).toLocaleString('de-CH', { - day: '2-digit', month: '2-digit', year: '2-digit', - hour: '2-digit', minute: '2-digit', second: '2-digit', - }) -} - -interface ConnectorRunDetailDrawerProps { - run: ConnectorRun | null - onClose: () => void -} - -export function ConnectorRunDetailDrawer({ run, onClose }: ConnectorRunDetailDrawerProps) { - return ( - - {run && ( - - {/* Header */} - - - Import-Run Details - - - - - - - - {/* Status */} - - - - {run.id} - - - - {/* Timestamps */} - - - Gestartet: {formatDate(run.startedAt)} - - {run.finishedAt && ( - - Beendet: {formatDate(run.finishedAt)} - - )} - - - - - {/* Stats grid */} - - {[ - { label: 'Erkannt', value: run.itemsDetected, color: '#1e293b' }, - { label: 'Normalisiert', value: run.itemsNormalized, color: '#15803d' }, - { label: 'Abgelehnt', value: run.itemsRejected, color: run.itemsRejected > 0 ? '#dc2626' : '#64748b' }, - { label: 'Signale erstellt', value: run.signalsCreated, color: '#7c3aed' }, - ].map(({ label, value, color }) => ( - - - {value} - - {label} - - ))} - - - {/* Summary */} - - - Zusammenfassung - - - {run.runSummary} - - - {/* Signals note */} - {run.signalsCreated > 0 && ( - - - - {run.signalsCreated} Marktsignal{run.signalsCreated !== 1 ? 'e' : ''} aus diesem Run verfügbar in Market Intelligence. - - - )} - - {/* Errors */} - {run.errors.length > 0 && ( - <> - - Fehler - - - {run.errors.map((err, i) => ( - - - {err} - - ))} - - - )} - - {/* Warnings */} - {run.warnings.length > 0 && ( - <> - - Warnungen - - - {run.warnings.map((w, i) => ( - - - {w} - - ))} - - - )} - - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/ConnectorRunTable.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/ConnectorRunTable.tsx deleted file mode 100644 index 649406a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/ConnectorRunTable.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Box, Chip, Table, TableBody, TableCell, TableHead, TableRow, Typography } from '@mui/material' -import type { ConnectorRun } from '../../domain/dataSource' -import { CONNECTOR_RUN_STATUS_LABELS, CONNECTOR_RUN_STATUS_COLORS } from '../../domain/dataSource' - -function formatDate(iso: string): string { - return new Date(iso).toLocaleString('de-CH', { - day: '2-digit', month: '2-digit', year: '2-digit', - hour: '2-digit', minute: '2-digit', - }) -} - -function formatDuration(start: string, end?: string): string { - if (!end) return 'läuft...' - const ms = new Date(end).getTime() - new Date(start).getTime() - if (ms < 60_000) return `${Math.round(ms / 1000)}s` - return `${Math.round(ms / 60_000)}m` -} - -interface ConnectorRunTableProps { - runs: ConnectorRun[] - onSelectRun: (run: ConnectorRun) => void -} - -export function ConnectorRunTable({ runs, onSelectRun }: ConnectorRunTableProps) { - if (runs.length === 0) { - return ( - - Keine Import-Runs vorhanden. - - ) - } - - return ( - - - - - Gestartet - Status - Erkannt - Normalisiert - Signale - Dauer - - - - {runs.map((run) => { - const { bg, fg } = CONNECTOR_RUN_STATUS_COLORS[run.status] - return ( - onSelectRun(run)} - sx={{ cursor: 'pointer', '& td': { fontSize: '0.78rem', py: 0.75 } }} - > - {formatDate(run.startedAt)} - - - - {run.itemsDetected} - {run.itemsNormalized} - - 0 ? '#7c3aed' : 'text.primary', fontWeight: run.signalsCreated > 0 ? 600 : 400 }}> - {run.signalsCreated} - - - - {formatDuration(run.startedAt, run.finishedAt)} - - - ) - })} - -
-
- ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/DataCategoryBadgeList.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/DataCategoryBadgeList.tsx deleted file mode 100644 index be25c05..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/DataCategoryBadgeList.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Box, Chip } from '@mui/material' - -interface DataCategoryBadgeListProps { - categories: string[] - max?: number -} - -export function DataCategoryBadgeList({ categories, max }: DataCategoryBadgeListProps) { - const visible = max ? categories.slice(0, max) : categories - const overflow = max ? Math.max(0, categories.length - max) : 0 - - return ( - - {visible.map((cat) => ( - - ))} - {overflow > 0 && ( - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/EvidenceGatePanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/EvidenceGatePanel.tsx deleted file mode 100644 index 637a0d1..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/EvidenceGatePanel.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Box, Chip, Divider, Paper, Typography } from '@mui/material' -import { CheckCircle2, ChevronRight, FileSearch, XCircle } from 'lucide-react' -import type { GateEvaluation } from '../../domain/signalPipeline' -import { GateStatus, GATE_STATUS_LABELS, GATE_STATUS_COLORS } from '../../domain/signalPipeline' - -interface EvidenceGatePanelProps { - gate: GateEvaluation -} - -export function EvidenceGatePanel({ gate }: EvidenceGatePanelProps) { - const { bg, fg } = GATE_STATUS_COLORS[gate.status] - - return ( - - - - - Evidenz-Gate - - - - - - - - {gate.checks.map((check, i) => ( - - {check.passed - ? - : - } - - {check.label} - {check.value && ( - - {check.value} - - )} - - - ))} - {gate.reason && ( - - {gate.reason} - - )} - {gate.nextAction && gate.status !== GateStatus.PASSED && ( - - - - {gate.nextAction} - - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/FeedEligibilityBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/FeedEligibilityBadge.tsx deleted file mode 100644 index cab496d..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/FeedEligibilityBadge.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Chip } from '@mui/material' -import { CheckCircle2, MinusCircle } from 'lucide-react' -import type { MarketSignal } from '../../domain/marketSignal' -import { SignalProcessingStatus } from '../../domain/marketSignal' -import { SensitivityLevel } from '../../domain/enums' - -interface FeedEligibilityBadgeProps { - signal: MarketSignal -} - -export function FeedEligibilityBadge({ signal }: FeedEligibilityBadgeProps) { - const ELIGIBLE_STATUSES: SignalProcessingStatus[] = [ - SignalProcessingStatus.APPROVED_AS_SIGNAL, - SignalProcessingStatus.CONVERTED_TO_FUTURE_AVAILABILITY, - ] - const BLOCKING_SENSITIVITY: SensitivityLevel[] = [ - SensitivityLevel.CONFIDENTIAL, - SensitivityLevel.RESTRICTED, - ] - - const isEligible = - ELIGIBLE_STATUSES.includes(signal.processingStatus) && - !BLOCKING_SENSITIVITY.includes(signal.sensitivityLevel) - - if (isEligible) { - return ( - } - sx={{ - bgcolor: 'rgba(22,163,74,0.1)', - color: '#15803d', - border: 'none', - fontSize: '0.7rem', - fontWeight: 600, - '& .MuiChip-icon': { ml: 0.75 }, - }} - /> - ) - } - - return ( - } - sx={{ - bgcolor: 'rgba(148,163,184,0.1)', - color: '#64748b', - border: 'none', - fontSize: '0.7rem', - fontWeight: 600, - '& .MuiChip-icon': { ml: 0.75 }, - }} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalCard.tsx deleted file mode 100644 index 46b7b07..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalCard.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { Box, Chip, Typography } from '@mui/material' -import { - Building2, FileText, Newspaper, TrendingUp, FileCheck2, - HardHat, Database, CalendarClock, Search, PenLine, -} from 'lucide-react' -import type { LucideIcon } from 'lucide-react' -import { - MARKET_SIGNAL_SOURCE_LABELS, - SIGNAL_PROCESSING_STATUS_LABELS, - SIGNAL_PROCESSING_STATUS_COLORS, - MarketSignalSourceCategory, -} from '../../domain/marketSignal' -import type { MarketSignal } from '../../domain/marketSignal' -import { SensitivityLevel } from '../../domain/enums' -import { SignalConfidenceBadge } from './SignalConfidenceBadge' - -const SOURCE_ICONS: Record = { - PUBLIC_LISTING_PLATFORM: Building2, - BUILDING_PERMIT_REGISTER: FileText, - COMPANY_NEWS: Newspaper, - JOB_GROWTH_SIGNAL: TrendingUp, - COMMERCIAL_REGISTER: FileCheck2, - INFRASTRUCTURE_PROJECT: HardHat, - PORTFOLIO_IMPORT: Database, - LEASE_EXPIRY_DATA: CalendarClock, - USER_DEMAND_SIGNAL: Search, - MANUAL_ANALYST_SIGNAL: PenLine, -} - -const SENSITIVITY_COLORS: Record = { - [SensitivityLevel.INTERNAL]: '#f59e0b', - [SensitivityLevel.CONFIDENTIAL]: '#ef4444', - [SensitivityLevel.RESTRICTED]: '#7c3aed', -} - -function formatDate(iso: string): string { - return new Date(iso).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: '2-digit' }) -} - -interface MarketSignalCardProps { - signal: MarketSignal - selected: boolean - onClick: () => void -} - -export function MarketSignalCard({ signal, selected, onClick }: MarketSignalCardProps) { - const Icon = SOURCE_ICONS[signal.sourceCategory] ?? Building2 - const { bg, fg } = SIGNAL_PROCESSING_STATUS_COLORS[signal.processingStatus] - const sensitivityColor = SENSITIVITY_COLORS[signal.sensitivityLevel] - - return ( - - {/* Row 1: source + sensitivity */} - - - - - {MARKET_SIGNAL_SOURCE_LABELS[signal.sourceCategory as MarketSignalSourceCategory]} - - - {sensitivityColor && ( - - )} - - - {/* Row 2: title */} - - {signal.title} - - - {/* Row 3: location + date */} - - {signal.location} · {formatDate(signal.detectedAt)} - - - {/* Row 4: status + confidence */} - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalDetailPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalDetailPanel.tsx deleted file mode 100644 index 4b1e21b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalDetailPanel.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { Box, Button, Chip, CircularProgress, Divider, Tooltip, Typography } from '@mui/material' -import { - Building2, FileText, Newspaper, TrendingUp, FileCheck2, - HardHat, Database, CalendarClock, Search, PenLine, CheckCircle2, - XCircle, Send, Link2, -} from 'lucide-react' -import type { LucideIcon } from 'lucide-react' -import type { MarketSignal } from '../../domain/marketSignal' -import { - MARKET_SIGNAL_SOURCE_LABELS, - SIGNAL_PROCESSING_STATUS_LABELS, - SIGNAL_PROCESSING_STATUS_COLORS, - SignalProcessingStatus, - ExtractedEntityType, -} from '../../domain/marketSignal' -import { SensitivityLevel } from '../../domain/enums' -import { FreshnessBadge } from '../badges/FreshnessBadge' -import { SourceReliabilityBadge } from './SourceReliabilityBadge' -import { SignalConfidenceBadge } from './SignalConfidenceBadge' -import { SensitivityWarningPanel } from './SensitivityWarningPanel' -import { SignalEvidenceList } from './SignalEvidenceList' -import { SignalConversionPanel } from './SignalConversionPanel' -import { MarketSignalEmptyState } from './MarketSignalEmptyState' -import { FeedEligibilityBadge } from './FeedEligibilityBadge' -import { useUpdateSignalStatus, useCreateReviewTask } from '../../hooks/useMarketSignals' - -const SOURCE_ICONS: Record = { - PUBLIC_LISTING_PLATFORM: Building2, - BUILDING_PERMIT_REGISTER: FileText, - COMPANY_NEWS: Newspaper, - JOB_GROWTH_SIGNAL: TrendingUp, - COMMERCIAL_REGISTER: FileCheck2, - INFRASTRUCTURE_PROJECT: HardHat, - PORTFOLIO_IMPORT: Database, - LEASE_EXPIRY_DATA: CalendarClock, - USER_DEMAND_SIGNAL: Search, - MANUAL_ANALYST_SIGNAL: PenLine, -} - -const ENTITY_TYPE_LABELS: Record = { - [ExtractedEntityType.COMPANY]: 'Unternehmen', - [ExtractedEntityType.PERSON]: 'Person', - [ExtractedEntityType.LOCATION]: 'Ort', - [ExtractedEntityType.ASSET]: 'Objekt', - [ExtractedEntityType.DATE]: 'Datum', -} - -const SENSITIVITY_LABELS: Record = { - [SensitivityLevel.PUBLIC]: 'Öffentlich', - [SensitivityLevel.INTERNAL]: 'Intern', - [SensitivityLevel.CONFIDENTIAL]: 'Vertraulich', - [SensitivityLevel.RESTRICTED]: 'Eingeschränkt', -} - -function formatDate(iso: string): string { - return new Date(iso).toLocaleString('de-CH', { - day: '2-digit', month: '2-digit', year: '2-digit', - hour: '2-digit', minute: '2-digit', - }) -} - -interface MarketSignalDetailPanelProps { - signal: MarketSignal | null -} - -export function MarketSignalDetailPanel({ signal }: MarketSignalDetailPanelProps) { - const { mutate: updateStatus, isPending: isUpdating } = useUpdateSignalStatus() - const { mutate: sendToReview, isPending: isSending } = useCreateReviewTask() - - if (!signal) return - - const SourceIcon = SOURCE_ICONS[signal.sourceCategory] ?? Building2 - const { bg, fg } = SIGNAL_PROCESSING_STATUS_COLORS[signal.processingStatus] - const NON_ACTIONABLE_STATUSES: SignalProcessingStatus[] = [ - SignalProcessingStatus.REJECTED, - SignalProcessingStatus.ARCHIVED, - SignalProcessingStatus.CONVERTED_TO_FUTURE_AVAILABILITY, - ] - const isActionable = !NON_ACTIONABLE_STATUSES.includes(signal.processingStatus) - - return ( - - {/* Header */} - - - - {signal.title} - - - - - - - - {MARKET_SIGNAL_SOURCE_LABELS[signal.sourceCategory]} · {signal.location} · {formatDate(signal.detectedAt)} - - - - - - - - - - - - - {/* Body */} - - - - {/* Summary */} - - {signal.summary} - - - - - {/* Evidence */} - - Quellenangaben & Evidenz - - - - {signal.extractedEntities.length > 0 && ( - <> - - - Extrahierte Entitäten - - - {signal.extractedEntities.map((entity, i) => ( - - - - ))} - - - )} - - {signal.analystNotes && ( - <> - - - Analyst-Notizen - - - {signal.analystNotes} - - - )} - - {/* Conversion panel */} - - - - {/* Actions */} - {isActionable && ( - <> - - - Aktionen - - - - - - - - - - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalEmptyState.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalEmptyState.tsx deleted file mode 100644 index aa421d4..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalEmptyState.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Box, Typography } from '@mui/material' -import { Inbox, MousePointerClick, SearchX, AlertTriangle } from 'lucide-react' - -type EmptyVariant = 'no-selection' | 'empty-inbox' | 'no-results' | 'not-found' - -const VARIANTS: Record = { - 'no-selection': { - icon: MousePointerClick, - title: 'Signal auswählen', - subtitle: 'Wählen Sie ein Signal aus der Liste, um die Details anzuzeigen.', - }, - 'empty-inbox': { - icon: Inbox, - title: 'Keine Signale', - subtitle: 'Es wurden noch keine Marktzeichen erkannt. Starten Sie eine Intelligence-Analyse.', - }, - 'no-results': { - icon: SearchX, - title: 'Keine Ergebnisse', - subtitle: 'Keine Signale entsprechen den aktiven Filtern. Filter anpassen oder zurücksetzen.', - }, - 'not-found': { - icon: AlertTriangle, - title: 'Signal nicht gefunden', - subtitle: 'Das ausgewählte Signal konnte nicht geladen werden.', - }, -} - -export function MarketSignalEmptyState({ variant }: { variant: EmptyVariant }) { - const { icon: Icon, title, subtitle } = VARIANTS[variant] - - return ( - - - - - - - {title} - - - {subtitle} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalFilterBar.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalFilterBar.tsx deleted file mode 100644 index 560e805..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalFilterBar.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { Box, Button, Chip, Typography } from '@mui/material' -import { X } from 'lucide-react' -import { - MarketSignalSourceCategory, - MARKET_SIGNAL_SOURCE_LABELS, - SignalProcessingStatus, - SIGNAL_PROCESSING_STATUS_LABELS, - SIGNAL_PROCESSING_STATUS_COLORS, -} from '../../domain/marketSignal' -import type { MarketSignalFilters } from '../../domain/marketSignal' - -interface MarketSignalFilterBarProps { - filters: MarketSignalFilters - onChange: (f: MarketSignalFilters) => void -} - -const SOURCE_OPTIONS = Object.values(MarketSignalSourceCategory) -const STATUS_OPTIONS: SignalProcessingStatus[] = [ - SignalProcessingStatus.NEEDS_REVIEW, - SignalProcessingStatus.ENRICHED, - SignalProcessingStatus.APPROVED_AS_SIGNAL, - SignalProcessingStatus.DETECTED, - SignalProcessingStatus.CONVERTED_TO_FUTURE_AVAILABILITY, - SignalProcessingStatus.REJECTED, -] - -export function MarketSignalFilterBar({ filters, onChange }: MarketSignalFilterBarProps) { - const hasFilters = !!(filters.sourceCategory || filters.processingStatus) - - return ( - - {/* Source category */} - - Quelle - - - {SOURCE_OPTIONS.map((cat) => { - const active = filters.sourceCategory === cat - return ( - - onChange({ ...filters, sourceCategory: active ? undefined : cat }) - } - sx={{ - fontSize: '0.65rem', - height: 20, - bgcolor: active ? '#1e3a5f' : 'transparent', - color: active ? '#fff' : 'text.secondary', - border: '1px solid', - borderColor: active ? '#1e3a5f' : 'divider', - '&:hover': { bgcolor: active ? '#162d4a' : 'rgba(0,0,0,0.04)' }, - }} - /> - ) - })} - - - {/* Processing status */} - - Status - - - {STATUS_OPTIONS.map((status) => { - const active = filters.processingStatus === status - const { bg, fg } = SIGNAL_PROCESSING_STATUS_COLORS[status] - return ( - - onChange({ ...filters, processingStatus: active ? undefined : status }) - } - sx={{ - fontSize: '0.65rem', - height: 20, - bgcolor: active ? bg : 'transparent', - color: active ? fg : 'text.secondary', - border: '1px solid', - borderColor: active ? fg : 'divider', - fontWeight: active ? 600 : 400, - '&:hover': { bgcolor: bg }, - }} - /> - ) - })} - - - {hasFilters && ( - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalSkeleton.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalSkeleton.tsx deleted file mode 100644 index c5b18d3..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/MarketSignalSkeleton.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Box, Skeleton } from '@mui/material' - -export function MarketSignalSkeleton() { - return ( - - - - - - - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/MatchabilityGatePanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/MatchabilityGatePanel.tsx deleted file mode 100644 index 355670e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/MatchabilityGatePanel.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Box, Chip, Divider, Paper, Typography } from '@mui/material' -import { CheckCircle2, ChevronRight, Layers, XCircle } from 'lucide-react' -import type { GateEvaluation } from '../../domain/signalPipeline' -import { GateStatus, GATE_STATUS_LABELS, GATE_STATUS_COLORS } from '../../domain/signalPipeline' - -interface MatchabilityGatePanelProps { - gate: GateEvaluation -} - -export function MatchabilityGatePanel({ gate }: MatchabilityGatePanelProps) { - const { bg, fg } = GATE_STATUS_COLORS[gate.status] - - return ( - - - - - Matchbarkeits-Gate - - - - - - - - {gate.checks.map((check, i) => ( - - {check.passed - ? - : - } - - {check.label} - {check.value && ( - - {check.value} - - )} - {check.note && ( - - – {check.note} - - )} - - - ))} - {gate.reason && ( - - {gate.reason} - - )} - {gate.nextAction && gate.status !== GateStatus.PASSED && ( - - - - {gate.nextAction} - - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/ReliabilityScorePanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/ReliabilityScorePanel.tsx deleted file mode 100644 index efade0f..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/ReliabilityScorePanel.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Box, LinearProgress, Tooltip, Typography } from '@mui/material' - -function scoreColor(score: number): string { - if (score >= 0.85) return '#15803d' - if (score >= 0.65) return '#a16207' - return '#dc2626' -} - -interface ReliabilityScorePanelProps { - score: number - compact?: boolean -} - -export function ReliabilityScorePanel({ score, compact = false }: ReliabilityScorePanelProps) { - const pct = Math.round(score * 100) - const color = scoreColor(score) - - if (compact) { - return ( - - - - {pct}% - - - ) - } - - return ( - - - - Source Reliability - - - {pct}% - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/ReviewGatePanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/ReviewGatePanel.tsx deleted file mode 100644 index b70ae71..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/ReviewGatePanel.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Box, Chip, Divider, Paper, Typography } from '@mui/material' -import { CheckCircle2, ChevronRight, ClipboardCheck, XCircle } from 'lucide-react' -import type { GateEvaluation } from '../../domain/signalPipeline' -import { GateStatus, GATE_STATUS_LABELS, GATE_STATUS_COLORS } from '../../domain/signalPipeline' - -interface ReviewGatePanelProps { - gate: GateEvaluation -} - -export function ReviewGatePanel({ gate }: ReviewGatePanelProps) { - const { bg, fg } = GATE_STATUS_COLORS[gate.status] - - const reviewerNotes = gate.checks.filter( - c => c.value && c.label.toLowerCase().includes('review') - ) - - return ( - - - - - Review-Gate - - - - - - - - {gate.checks.map((check, i) => ( - - {check.passed - ? - : - } - - {check.label} - {check.value && ( - - {check.value} - - )} - - - ))} - - {reviewerNotes.length > 0 && ( - - - Reviewer-Informationen - - {reviewerNotes.map((note, i) => ( - - {note.label}: {note.value} - - ))} - - )} - - {gate.reason && ( - - {gate.reason} - - )} - {gate.nextAction && gate.status !== GateStatus.PASSED && ( - - - - {gate.nextAction} - - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SensitivityGatePanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SensitivityGatePanel.tsx deleted file mode 100644 index 918f890..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SensitivityGatePanel.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Alert, Box, Chip, Divider, Paper, Typography } from '@mui/material' -import { CheckCircle2, ChevronRight, ShieldCheck, XCircle } from 'lucide-react' -import type { GateEvaluation } from '../../domain/signalPipeline' -import { GateStatus, GATE_STATUS_LABELS, GATE_STATUS_COLORS } from '../../domain/signalPipeline' - -interface SensitivityGatePanelProps { - gate: GateEvaluation -} - -export function SensitivityGatePanel({ gate }: SensitivityGatePanelProps) { - const { bg, fg } = GATE_STATUS_COLORS[gate.status] - - const confidentialCheck = gate.checks.find(c => c.label === 'Nicht CONFIDENTIAL' && !c.passed) - const restrictedCheck = gate.checks.find(c => c.label === 'Nicht RESTRICTED' && !c.passed) - - return ( - - - - - Sensitivitäts-Gate - - - - - - - - {gate.checks.map((check, i) => ( - - {check.passed - ? - : - } - - {check.label} - {check.value && ( - - {check.value} - - )} - - - ))} - - {gate.status === GateStatus.FAILED && ( - - {confidentialCheck && ( - - Sensitivitätsstufe CONFIDENTIAL: Dieses Signal darf nicht im Demand Feed erscheinen. - Mieter- oder Vertragsidentitäten sind schützenswert und nur intern zugänglich. - - )} - {restrictedCheck && ( - - Sensitivitätsstufe RESTRICTED: Zugriff auf dieses Signal ist rollenbasiert eingeschränkt. - Nur autorisierte Nutzer mit entsprechender Berechtigung dürfen es einsehen. - - )} - - )} - - {gate.reason && ( - - {gate.reason} - - )} - {gate.nextAction && gate.status !== GateStatus.PASSED && ( - - - - {gate.nextAction} - - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SensitivityWarningPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SensitivityWarningPanel.tsx deleted file mode 100644 index f54fc21..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SensitivityWarningPanel.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Alert, AlertTitle } from '@mui/material' -import { Lock } from 'lucide-react' -import { SensitivityLevel } from '../../domain/enums' - -interface SensitivityWarningPanelProps { - sensitivityLevel: SensitivityLevel -} - -export function SensitivityWarningPanel({ sensitivityLevel }: SensitivityWarningPanelProps) { - if ( - sensitivityLevel === SensitivityLevel.PUBLIC || - sensitivityLevel === SensitivityLevel.INTERNAL - ) { - return null - } - - const isConfidential = sensitivityLevel === SensitivityLevel.CONFIDENTIAL - - return ( - } - sx={{ mb: 2 }} - > - - {isConfidential ? 'Vertrauliche Daten' : 'Zugriff eingeschränkt'} - - {isConfidential - ? 'Dieser Hinweis enthält vertrauliche Informationen aus internen oder Partnerdaten. Keine Weitergabe an Demand User. Nur für berechtigte Personen sichtbar.' - : 'Der Zugriff auf diesen Hinweis ist eingeschränkt. Verbreitung und Veröffentlichung ohne Freigabe nicht zulässig.'} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalConfidenceBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SignalConfidenceBadge.tsx deleted file mode 100644 index 4cff95e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalConfidenceBadge.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Chip } from '@mui/material' -import { Target } from 'lucide-react' - -interface SignalConfidenceBadgeProps { - score: number - size?: 'small' | 'medium' -} - -function getColor(score: number): { bg: string; fg: string } { - if (score >= 0.8) return { bg: 'rgba(34,197,94,0.12)', fg: '#16a34a' } - if (score >= 0.6) return { bg: 'rgba(99,102,241,0.12)', fg: '#4f46e5' } - if (score >= 0.4) return { bg: 'rgba(245,158,11,0.12)', fg: '#d97706' } - return { bg: 'rgba(239,68,68,0.12)', fg: '#dc2626' } -} - -export function SignalConfidenceBadge({ score, size = 'small' }: SignalConfidenceBadgeProps) { - const { bg, fg } = getColor(score) - return ( - } - label={`Konfidenz ${Math.round(score * 100)}%`} - sx={{ - bgcolor: bg, - color: fg, - border: 'none', - fontWeight: 600, - fontSize: size === 'small' ? '0.7rem' : '0.75rem', - }} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalConversionPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SignalConversionPanel.tsx deleted file mode 100644 index 86834cf..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalConversionPanel.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Box, Button, CircularProgress, Divider, Typography } from '@mui/material' -import { ArrowRight, Zap } from 'lucide-react' -import type { MarketSignal } from '../../domain/marketSignal' -import { SignalProcessingStatus } from '../../domain/marketSignal' -import { useConvertToFutureSignal } from '../../hooks/useMarketSignals' - -const ELIGIBLE_STATUSES: SignalProcessingStatus[] = [ - SignalProcessingStatus.ENRICHED, - SignalProcessingStatus.APPROVED_AS_SIGNAL, -] - -interface SignalConversionPanelProps { - signal: MarketSignal -} - -export function SignalConversionPanel({ signal }: SignalConversionPanelProps) { - const { mutate: convert, isPending, isSuccess } = useConvertToFutureSignal() - - if (!ELIGIBLE_STATUSES.includes(signal.processingStatus)) return null - - if (signal.possibleFutureSignalId || isSuccess) { - return ( - - - - Bereits in Future Availability überführt - {signal.possibleFutureSignalId && ` (${signal.possibleFutureSignalId})`} - - - ) - } - - return ( - - - - - In Future Availability überführen - - - - - - - Vorgeschlagener Titel: {signal.title} - - - Begründung: Signal hat ausreichend Evidenz und Source Reliability, um als - Future Availability Kandidat geführt zu werden. Manueller Review empfohlen. - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalEvidenceList.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SignalEvidenceList.tsx deleted file mode 100644 index d3bdce2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalEvidenceList.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Box, Chip, Link, Typography } from '@mui/material' -import { FileText, Globe, PenLine, FileArchive } from 'lucide-react' -import type { LucideIcon } from 'lucide-react' -import type { SignalEvidence } from '../../domain/marketSignal' -import { EvidenceType } from '../../domain/marketSignal' - -const EVIDENCE_ICONS: Record = { - [EvidenceType.TEXT_EXCERPT]: FileText, - [EvidenceType.URL_REFERENCE]: Globe, - [EvidenceType.ANALYST_NOTE]: PenLine, - [EvidenceType.DOCUMENT]: FileArchive, -} - -const EVIDENCE_LABELS: Record = { - [EvidenceType.TEXT_EXCERPT]: 'Textauszug', - [EvidenceType.URL_REFERENCE]: 'URL-Referenz', - [EvidenceType.ANALYST_NOTE]: 'Analyst-Notiz', - [EvidenceType.DOCUMENT]: 'Dokument', -} - -function formatDate(iso: string): string { - return new Date(iso).toLocaleString('de-CH', { - day: '2-digit', month: '2-digit', year: '2-digit', - hour: '2-digit', minute: '2-digit', - }) -} - -interface SignalEvidenceListProps { - evidence: SignalEvidence[] -} - -export function SignalEvidenceList({ evidence }: SignalEvidenceListProps) { - if (evidence.length === 0) { - return ( - - Keine Evidenz vorhanden. - - ) - } - - return ( - - {evidence.map((ev) => { - const Icon = EVIDENCE_ICONS[ev.evidenceType] ?? FileText - return ( - - - - - {EVIDENCE_LABELS[ev.evidenceType]} - - - - - - {ev.content} - - - {ev.sourceUrl && ( - - {ev.sourceUrl} - - )} - - - Abgerufen: {formatDate(ev.retrievedAt)} - - - ) - })} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalInbox.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SignalInbox.tsx deleted file mode 100644 index f7a8875..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalInbox.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { Box, Chip, TextField, Typography } from '@mui/material' -import { Search } from 'lucide-react' -import type { MarketSignal, MarketSignalFilters } from '../../domain/marketSignal' -import { MarketSignalCard } from './MarketSignalCard' -import { MarketSignalFilterBar } from './MarketSignalFilterBar' -import { MarketSignalSkeleton } from './MarketSignalSkeleton' -import { MarketSignalEmptyState } from './MarketSignalEmptyState' - -interface SignalInboxProps { - signals: MarketSignal[] - isLoading: boolean - selectedId: string | null - onSelect: (id: string) => void - filters: MarketSignalFilters - onFiltersChange: (f: MarketSignalFilters) => void -} - -export function SignalInbox({ - signals, - isLoading, - selectedId, - onSelect, - filters, - onFiltersChange, -}: SignalInboxProps) { - return ( - - {/* Inbox header */} - - - Signal-Inbox - - - - - {/* Search */} - - onFiltersChange({ ...filters, search: e.target.value || undefined })} - slotProps={{ - input: { - startAdornment: , - }, - }} - sx={{ '& .MuiOutlinedInput-root': { fontSize: '0.8rem' } }} - /> - - - {/* Filters */} - - - - - {/* Signal list */} - - {isLoading ? ( - Array.from({ length: 5 }).map((_, i) => ) - ) : signals.length === 0 ? ( - - ) : ( - signals.map((signal) => ( - onSelect(signal.id)} - /> - )) - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalPipelineStepper.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SignalPipelineStepper.tsx deleted file mode 100644 index bfb400e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalPipelineStepper.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Stepper, Step, StepLabel } from '@mui/material' -import type { PipelineState } from '../../domain/signalPipeline' -import { PIPELINE_STAGE_ORDER, PIPELINE_STAGE_LABELS } from '../../domain/signalPipeline' - -interface SignalPipelineStepperProps { - pipelineState: PipelineState -} - -export function SignalPipelineStepper({ pipelineState }: SignalPipelineStepperProps) { - const currentStageIndex = PIPELINE_STAGE_ORDER.indexOf(pipelineState.currentStage) - - return ( - - {PIPELINE_STAGE_ORDER.map((stage) => ( - - {PIPELINE_STAGE_LABELS[stage]} - - ))} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalPipelineView.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SignalPipelineView.tsx deleted file mode 100644 index 6906514..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalPipelineView.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { Alert, Box, Button, CircularProgress, Divider, Typography } from '@mui/material' -import { Rocket } from 'lucide-react' -import type { MarketSignal } from '../../domain/marketSignal' -import { GateStatus } from '../../domain/signalPipeline' -import { - useSignalPipelineState, - useSignalAuditTrail, - usePublishToFutureAvailability, -} from '../../hooks/useSignalPipeline' -import { FeedEligibilityBadge } from './FeedEligibilityBadge' -import { SignalPipelineStepper } from './SignalPipelineStepper' -import { EvidenceGatePanel } from './EvidenceGatePanel' -import { ConfidenceGatePanel } from './ConfidenceGatePanel' -import { SensitivityGatePanel } from './SensitivityGatePanel' -import { ReviewGatePanel } from './ReviewGatePanel' -import { MatchabilityGatePanel } from './MatchabilityGatePanel' -import { SignalToMatchAuditTrail } from './SignalToMatchAuditTrail' - -interface SignalPipelineViewProps { - signal: MarketSignal -} - -export function SignalPipelineView({ signal }: SignalPipelineViewProps) { - const { data: pipelineState, isLoading: isLoadingPipeline } = useSignalPipelineState(signal.id) - const { data: auditTrail = [], isLoading: isLoadingAudit } = useSignalAuditTrail(signal.id) - const { mutate: publish, isPending: isPublishing } = usePublishToFutureAvailability() - - const canPublish = - pipelineState !== null && - pipelineState !== undefined && - pipelineState.gates.REVIEW_GATE.status === GateStatus.PASSED && - !pipelineState.publishedToFutureAvailability - - return ( - - {/* Header */} - - - {signal.title} - - - - {pipelineState?.feedDisclaimer && ( - - {pipelineState.feedDisclaimer} - - )} - - - - {/* Pipeline Stepper */} - - {isLoadingPipeline && ( - - - - )} - {pipelineState && } - - - {/* Gate panels */} - - {isLoadingPipeline && !pipelineState && ( - - - - )} - - {pipelineState && ( - <> - - Gate-Bewertungen - - - - - - - - - {/* Feed eligibility summary */} - {pipelineState.overallEligible && pipelineState.publishedToFutureAvailability && ( - - Signal ist im Future Availability Feed publiziert - {pipelineState.publishedAt && ( - <> · {new Date(pipelineState.publishedAt).toLocaleString('de-CH', { - day: '2-digit', month: '2-digit', year: '2-digit', - hour: '2-digit', minute: '2-digit', - })} - )} - - )} - - {pipelineState.overallEligible && !pipelineState.publishedToFutureAvailability && ( - - Signal ist feed-fähig – noch nicht publiziert. - - )} - - {/* Publish button */} - {canPublish && ( - - - - )} - - - - - Audit Trail - - {isLoadingAudit ? ( - - - - ) : ( - - )} - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalToMatchAuditTrail.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SignalToMatchAuditTrail.tsx deleted file mode 100644 index 3a19dd1..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SignalToMatchAuditTrail.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Box, Typography } from '@mui/material' -import type { AuditTrailEntry } from '../../domain/signalPipeline' -import { PIPELINE_STAGE_ORDER } from '../../domain/signalPipeline' - -interface SignalToMatchAuditTrailProps { - entries: AuditTrailEntry[] -} - -function getStageColor(entry: AuditTrailEntry): string { - const idx = PIPELINE_STAGE_ORDER.indexOf(entry.stage) - if (idx <= 0) return '#94a3b8' - if (idx >= 5) return '#15803d' - if (idx >= 3) return '#1e3a5f' - if (idx >= 2) return '#4f46e5' - return '#2563eb' -} - -function formatTimestamp(iso: string): string { - return new Date(iso).toLocaleString('de-CH', { - day: '2-digit', - month: '2-digit', - year: '2-digit', - hour: '2-digit', - minute: '2-digit', - }) -} - -export function SignalToMatchAuditTrail({ entries }: SignalToMatchAuditTrailProps) { - if (entries.length === 0) { - return ( - - Keine Audit-Einträge vorhanden. - - ) - } - - return ( - - {entries.map((entry, idx) => { - const isLast = idx === entries.length - 1 - const dotColor = getStageColor(entry) - - return ( - - {/* Timeline column */} - - - {!isLast && ( - - )} - - - {/* Content column */} - - - {entry.action} - - - {entry.performedBy} · {formatTimestamp(entry.timestamp)} - - - {entry.details} - - - - ) - })} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SourceCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SourceCard.tsx deleted file mode 100644 index 39f31a1..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SourceCard.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { Box, Chip, Typography } from '@mui/material' -import { - Plug2, FileSpreadsheet, Upload, Globe, Rss, - Database, FileText, PenLine, Bot, -} from 'lucide-react' -import type { LucideIcon } from 'lucide-react' -import type { DataSource } from '../../domain/dataSource' -import { DATA_SOURCE_TYPE_LABELS } from '../../domain/dataSource' -import { SourceHealthBadge } from './SourceHealthBadge' -import { TermsStatusBadge } from './TermsStatusBadge' -import { ReliabilityScorePanel } from './ReliabilityScorePanel' - -const TYPE_ICONS: Record = { - API_CONNECTOR: Plug2, - CSV_IMPORT: FileSpreadsheet, - MANUAL_UPLOAD: Upload, - PUBLIC_WEB_SOURCE: Globe, - PARTNER_FEED: Rss, - INTERNAL_PORTFOLIO_EXPORT: Database, - CONTRACT_METADATA_IMPORT: FileText, - ANALYST_ENTRY: PenLine, - FUTURE_CRAWLER_STUB: Bot, -} - -function formatDate(iso?: string): string { - if (!iso) return '—' - return new Date(iso).toLocaleString('de-CH', { - day: '2-digit', month: '2-digit', year: '2-digit', - hour: '2-digit', minute: '2-digit', - }) -} - -interface SourceCardProps { - source: DataSource - selected: boolean - onClick: () => void -} - -export function SourceCard({ source, selected, onClick }: SourceCardProps) { - const Icon = TYPE_ICONS[source.sourceType] ?? Database - - return ( - - - - - - - - {source.name} - - - {DATA_SOURCE_TYPE_LABELS[source.sourceType]} - - - - - - - - - - - - - {formatDate(source.lastRunAt)} - - - - {source.regionCoverage.length > 0 && ( - - {source.regionCoverage.slice(0, 3).map((r) => ( - - ))} - {source.regionCoverage.length > 3 && ( - - +{source.regionCoverage.length - 3} - - )} - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SourceDetailPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SourceDetailPanel.tsx deleted file mode 100644 index c0511e2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SourceDetailPanel.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import { useState } from 'react' -import { - Box, Button, Chip, CircularProgress, Divider, Tooltip, Typography, -} from '@mui/material' -import { - Plug2, FileSpreadsheet, Upload, Globe, Rss, - Database, FileText, PenLine, Bot, - Play, Pause, Scale, RefreshCw, ExternalLink, -} from 'lucide-react' -import type { LucideIcon } from 'lucide-react' -import type { DataSource, ConnectorRun } from '../../domain/dataSource' -import { - DATA_SOURCE_TYPE_LABELS, - SourceStatus, - TermsStatus, -} from '../../domain/dataSource' -import { FreshnessBadge } from '../badges/FreshnessBadge' -import { SourceHealthBadge } from './SourceHealthBadge' -import { TermsStatusBadge } from './TermsStatusBadge' -import { ReliabilityScorePanel } from './ReliabilityScorePanel' -import { DataCategoryBadgeList } from './DataCategoryBadgeList' -import { SourceErrorPanel } from './SourceErrorPanel' -import { ConnectorRunTable } from './ConnectorRunTable' -import { ConnectorRunDetailDrawer } from './ConnectorRunDetailDrawer' -import { MarketSignalEmptyState } from './MarketSignalEmptyState' -import { - useConnectorRuns, - useTriggerMockRun, - useUpdateSourceStatus, - useMarkTermsStatus, -} from '../../hooks/useDataSources' - -const TYPE_ICONS: Record = { - API_CONNECTOR: Plug2, - CSV_IMPORT: FileSpreadsheet, - MANUAL_UPLOAD: Upload, - PUBLIC_WEB_SOURCE: Globe, - PARTNER_FEED: Rss, - INTERNAL_PORTFOLIO_EXPORT: Database, - CONTRACT_METADATA_IMPORT: FileText, - ANALYST_ENTRY: PenLine, - FUTURE_CRAWLER_STUB: Bot, -} - -function formatDate(iso?: string): string { - if (!iso) return '—' - return new Date(iso).toLocaleString('de-CH', { - day: '2-digit', month: '2-digit', year: '2-digit', - hour: '2-digit', minute: '2-digit', - }) -} - -interface SourceDetailPanelProps { - source: DataSource | null -} - -export function SourceDetailPanel({ source }: SourceDetailPanelProps) { - const [selectedRun, setSelectedRun] = useState(null) - - const { data: runs = [] } = useConnectorRuns(source?.id ?? null) - const { mutate: triggerRun, isPending: isRunning } = useTriggerMockRun() - const { mutate: updateStatus, isPending: isUpdating } = useUpdateSourceStatus() - const { mutate: markTerms, isPending: isMarkingTerms } = useMarkTermsStatus() - - if (!source) return - - const SourceIcon = TYPE_ICONS[source.sourceType] ?? Database - const isActionable = source.status !== SourceStatus.DISABLED - const RUN_ELIGIBLE: SourceStatus[] = [SourceStatus.ACTIVE, SourceStatus.ERROR] - const canRun = RUN_ELIGIBLE.includes(source.status) - - return ( - - {/* Header */} - - - - - - - - {source.name} - - - {DATA_SOURCE_TYPE_LABELS[source.sourceType]} - {source.ownerOrganizationId && ` · Org ${source.ownerOrganizationId}`} - - - - - - - - - - - - {/* Body */} - - {source.errorState && } - - {/* Legal basis */} - - - Rechtliche Grundlage - - - {source.legalBasis} - - - - {/* Reliability */} - - - - - {/* Run timestamps */} - - - Letzter Run - {formatDate(source.lastRunAt)} - - - Nächster Run - {formatDate(source.nextRunAt)} - - - - - - {/* Data categories */} - Datenkategorien - - - - - {/* Region coverage */} - Regionen - - {source.regionCoverage.map((r) => ( - - ))} - - - {/* Asset types */} - Asset-Typen - - {source.supportedAssetTypes.map((a) => ( - - ))} - - - {source.notes && ( - <> - - Notizen - - {source.notes} - - - )} - - {/* Actions */} - {isActionable && ( - <> - - Aktionen - - {canRun && ( - - )} - {source.status === SourceStatus.ACTIVE && ( - - )} - {source.status === SourceStatus.PAUSED && ( - - )} - {source.termsStatus !== TermsStatus.NEEDS_LEGAL_REVIEW && ( - - - - )} - - - - - - )} - - {/* Connector runs */} - - - Import-Runs - - - - - setSelectedRun(null)} /> - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SourceErrorPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SourceErrorPanel.tsx deleted file mode 100644 index 839e11a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SourceErrorPanel.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Alert, Typography } from '@mui/material' -import { AlertCircle } from 'lucide-react' - -interface SourceErrorPanelProps { - errorState: string -} - -export function SourceErrorPanel({ errorState }: SourceErrorPanelProps) { - return ( - } - sx={{ mb: 2, fontSize: '0.8rem', '& .MuiAlert-message': { width: '100%' } }} - > - - Fehlerzustand - - - {errorState} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SourceHealthBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SourceHealthBadge.tsx deleted file mode 100644 index e74384c..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SourceHealthBadge.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Chip } from '@mui/material' -import { CheckCircle2, PauseCircle, AlertCircle, Clock, XCircle } from 'lucide-react' -import type { LucideIcon } from 'lucide-react' -import type { SourceStatus } from '../../domain/dataSource' -import { SOURCE_STATUS_LABELS, SOURCE_STATUS_COLORS } from '../../domain/dataSource' - -const STATUS_ICONS: Record = { - ACTIVE: CheckCircle2, - PAUSED: PauseCircle, - ERROR: AlertCircle, - PENDING_REVIEW: Clock, - DISABLED: XCircle, -} - -interface SourceHealthBadgeProps { - status: SourceStatus -} - -export function SourceHealthBadge({ status }: SourceHealthBadgeProps) { - const Icon = STATUS_ICONS[status] - const { bg, fg } = SOURCE_STATUS_COLORS[status] - return ( - } - label={SOURCE_STATUS_LABELS[status]} - sx={{ - bgcolor: bg, - color: fg, - border: 'none', - fontSize: '0.7rem', - fontWeight: 600, - '& .MuiChip-icon': { ml: 0.75 }, - }} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SourceList.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SourceList.tsx deleted file mode 100644 index 8e30956..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SourceList.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { Box, Chip, MenuItem, Select, TextField, Typography } from '@mui/material' -import { Search } from 'lucide-react' -import type { DataSource, SourceFilters } from '../../domain/dataSource' -import { - DataSourceType, - SourceStatus, - DATA_SOURCE_TYPE_LABELS, - SOURCE_STATUS_LABELS, -} from '../../domain/dataSource' -import { SourceCard } from './SourceCard' -import { MarketSignalSkeleton } from './MarketSignalSkeleton' -import { MarketSignalEmptyState } from './MarketSignalEmptyState' - -interface SourceListProps { - sources: DataSource[] - isLoading: boolean - selectedId: string | null - onSelect: (id: string) => void - filters: SourceFilters - onFiltersChange: (f: SourceFilters) => void -} - -export function SourceList({ - sources, - isLoading, - selectedId, - onSelect, - filters, - onFiltersChange, -}: SourceListProps) { - const hasActiveFilters = filters.sourceType || filters.status || filters.search - - return ( - - {/* Header */} - - - Datenquellen - - - - - {/* Search */} - - onFiltersChange({ ...filters, search: e.target.value || undefined })} - slotProps={{ - input: { - startAdornment: , - }, - }} - sx={{ '& .MuiOutlinedInput-root': { fontSize: '0.8rem' } }} - /> - - - {/* Filters */} - - - - - - {hasActiveFilters && ( - - onFiltersChange({})} - sx={{ fontSize: '0.7rem', cursor: 'pointer' }} - /> - - )} - - {/* List */} - - {isLoading ? ( - Array.from({ length: 4 }).map((_, i) => ) - ) : sources.length === 0 ? ( - - ) : ( - sources.map((source) => ( - onSelect(source.id)} - /> - )) - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/SourceReliabilityBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/SourceReliabilityBadge.tsx deleted file mode 100644 index 6f1b9c1..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/SourceReliabilityBadge.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Chip } from '@mui/material' -import { ShieldCheck } from 'lucide-react' - -interface SourceReliabilityBadgeProps { - score: number - size?: 'small' | 'medium' -} - -function getColor(score: number): { bg: string; fg: string } { - if (score >= 0.8) return { bg: 'rgba(34,197,94,0.12)', fg: '#16a34a' } - if (score >= 0.6) return { bg: 'rgba(245,158,11,0.12)', fg: '#d97706' } - return { bg: 'rgba(239,68,68,0.12)', fg: '#dc2626' } -} - -export function SourceReliabilityBadge({ score, size = 'small' }: SourceReliabilityBadgeProps) { - const { bg, fg } = getColor(score) - return ( - } - label={`Quelle ${Math.round(score * 100)}%`} - sx={{ - bgcolor: bg, - color: fg, - border: 'none', - fontWeight: 600, - fontSize: size === 'small' ? '0.7rem' : '0.75rem', - }} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/TermsStatusBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/ops/TermsStatusBadge.tsx deleted file mode 100644 index 2851b30..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/TermsStatusBadge.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Chip } from '@mui/material' -import { ShieldCheck, AlertTriangle, ShieldAlert, ShieldOff, HelpCircle } from 'lucide-react' -import type { LucideIcon } from 'lucide-react' -import type { TermsStatus } from '../../domain/dataSource' -import { TERMS_STATUS_LABELS, TERMS_STATUS_COLORS } from '../../domain/dataSource' - -const TERMS_ICONS: Record = { - APPROVED: ShieldCheck, - NEEDS_LEGAL_REVIEW: AlertTriangle, - RESTRICTED: ShieldAlert, - BLOCKED: ShieldOff, - UNKNOWN: HelpCircle, -} - -interface TermsStatusBadgeProps { - status: TermsStatus -} - -export function TermsStatusBadge({ status }: TermsStatusBadgeProps) { - const Icon = TERMS_ICONS[status] - const { bg, fg, border } = TERMS_STATUS_COLORS[status] - return ( - } - label={TERMS_STATUS_LABELS[status]} - sx={{ - bgcolor: bg, - color: fg, - border: '1px solid', - borderColor: border, - fontSize: '0.7rem', - fontWeight: 600, - '& .MuiChip-icon': { ml: 0.75 }, - }} - /> - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ops/index.ts b/.claude/worktrees/agent-a82a3716/src/components/ops/index.ts deleted file mode 100644 index b975e22..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ops/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -export { SourceReliabilityBadge } from './SourceReliabilityBadge' -export { SignalConfidenceBadge } from './SignalConfidenceBadge' -export { SensitivityWarningPanel } from './SensitivityWarningPanel' -export { MarketSignalEmptyState } from './MarketSignalEmptyState' -export { MarketSignalSkeleton } from './MarketSignalSkeleton' -export { MarketSignalFilterBar } from './MarketSignalFilterBar' -export { MarketSignalCard } from './MarketSignalCard' -export { SignalEvidenceList } from './SignalEvidenceList' -export { SignalConversionPanel } from './SignalConversionPanel' -export { SignalInbox } from './SignalInbox' -export { MarketSignalDetailPanel } from './MarketSignalDetailPanel' -export { SourceHealthBadge } from './SourceHealthBadge' -export { TermsStatusBadge } from './TermsStatusBadge' -export { ReliabilityScorePanel } from './ReliabilityScorePanel' -export { DataCategoryBadgeList } from './DataCategoryBadgeList' -export { SourceErrorPanel } from './SourceErrorPanel' -export { SourceCard } from './SourceCard' -export { SourceList } from './SourceList' -export { ConnectorRunTable } from './ConnectorRunTable' -export { ConnectorRunDetailDrawer } from './ConnectorRunDetailDrawer' -export { SourceDetailPanel } from './SourceDetailPanel' -export { FeedEligibilityBadge } from './FeedEligibilityBadge' -export { SignalPipelineStepper } from './SignalPipelineStepper' -export { EvidenceGatePanel } from './EvidenceGatePanel' -export { ConfidenceGatePanel } from './ConfidenceGatePanel' -export { SensitivityGatePanel } from './SensitivityGatePanel' -export { ReviewGatePanel } from './ReviewGatePanel' -export { MatchabilityGatePanel } from './MatchabilityGatePanel' -export { SignalToMatchAuditTrail } from './SignalToMatchAuditTrail' -export { SignalPipelineView } from './SignalPipelineView' diff --git a/.claude/worktrees/agent-a82a3716/src/components/panels/DetailPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/panels/DetailPanel.tsx deleted file mode 100644 index 40df508..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/panels/DetailPanel.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Accordion, AccordionDetails, AccordionSummary, Paper, Typography } from '@mui/material' -import { ChevronDown } from 'lucide-react' -import type { SxProps, Theme } from '@mui/material' -import type { ReactNode } from 'react' - -interface DetailSection { - title: string - defaultExpanded?: boolean - children: ReactNode -} - -interface DetailPanelProps { - sections: DetailSection[] - sx?: SxProps -} - -export function DetailPanel({ sections, sx }: DetailPanelProps) { - return ( - - {sections.map((section, i) => ( - - } - sx={{ px: 2, py: 0, minHeight: 44, '& .MuiAccordionSummary-content': { my: 0 } }} - > - - {section.title} - - - - {section.children} - - - ))} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/panels/InfoPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/panels/InfoPanel.tsx deleted file mode 100644 index 9725739..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/panels/InfoPanel.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Box, Divider, Paper, Typography } from '@mui/material' -import type { SxProps, Theme } from '@mui/material' -import type { ReactNode } from 'react' - -interface InfoPanelProps { - title?: string - icon?: ReactNode - children: ReactNode - sx?: SxProps -} - -export function InfoPanel({ title, icon, children, sx }: InfoPanelProps) { - return ( - - {title && ( - <> - - {icon} - - {title} - - - - - )} - {children} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/panels/WarningPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/panels/WarningPanel.tsx deleted file mode 100644 index 01702ef..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/panels/WarningPanel.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Alert, AlertTitle, Box } from '@mui/material' -import type { ReactNode } from 'react' - -interface WarningPanelProps { - title: string - description?: string - severity: 'warning' | 'critical' - actions?: ReactNode - children?: ReactNode -} - -export function WarningPanel({ title, description, severity, actions, children }: WarningPanelProps) { - return ( - - - {title} - - {description && {description}} - {children && {children}} - {actions && {actions}} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/panels/index.ts b/.claude/worktrees/agent-a82a3716/src/components/panels/index.ts deleted file mode 100644 index 18b5183..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/panels/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { InfoPanel } from './InfoPanel' -export { DetailPanel } from './DetailPanel' -export { WarningPanel } from './WarningPanel' diff --git a/.claude/worktrees/agent-a82a3716/src/components/results/FeedEmptyState.tsx b/.claude/worktrees/agent-a82a3716/src/components/results/FeedEmptyState.tsx deleted file mode 100644 index e3da2a5..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/results/FeedEmptyState.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { EmptyState } from '../ui' - -export function FeedEmptyState({ filtered }: { filtered?: boolean }) { - return ( - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/results/FeedSkeleton.tsx b/.claude/worktrees/agent-a82a3716/src/components/results/FeedSkeleton.tsx deleted file mode 100644 index bfd8f95..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/results/FeedSkeleton.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Card, Skeleton, Stack } from '@mui/material' - -export function FeedSkeleton() { - return ( - <> - {[1, 2, 3].map(i => ( - - - - - - - - - - - - - ))} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/results/ResultConfidenceSummary.tsx b/.claude/worktrees/agent-a82a3716/src/components/results/ResultConfidenceSummary.tsx deleted file mode 100644 index 1ab9577..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/results/ResultConfidenceSummary.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Box, LinearProgress, Stack, Typography } from '@mui/material' - -interface Props { - confidenceLevel: number - dataQualityScore?: number -} - -export function ResultConfidenceSummary({ confidenceLevel, dataQualityScore }: Props) { - const confColor = confidenceLevel >= 0.8 ? '#1a7a4a' : confidenceLevel >= 0.6 ? '#d97706' : '#c0392b' - return ( - - - Konfidenz - {Math.round(confidenceLevel * 100)}% - - {dataQualityScore !== undefined && ( - - Datenqualität - - - - - {Math.round(dataQualityScore * 100)}% - - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/results/ResultFeedHeader.tsx b/.claude/worktrees/agent-a82a3716/src/components/results/ResultFeedHeader.tsx deleted file mode 100644 index daae81b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/results/ResultFeedHeader.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Box, Typography } from '@mui/material' - -interface Props { - total: number - verifiedCount: number - externalCount: number - futureCount: number -} - -export function ResultFeedHeader({ total, verifiedCount, externalCount, futureCount }: Props) { - return ( - - - {total} Treffer gefunden - - - {verifiedCount} Verified · {externalCount} Extern · {futureCount} Signale - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/results/ResultFilterBar.tsx b/.claude/worktrees/agent-a82a3716/src/components/results/ResultFilterBar.tsx deleted file mode 100644 index 9e40933..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/results/ResultFilterBar.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Box, Card, Chip, Stack, Typography } from '@mui/material' -import type { ResultType } from '../../domain/enums' - -type FilterSource = ResultType | 'ALL' -type SortBy = 'score' | 'rent' | 'area' - -interface Props { - filterSource: FilterSource - onFilterChange: (v: FilterSource) => void - sortBy: SortBy - onSortChange: (v: SortBy) => void -} - -const FILTER_OPTIONS: { value: FilterSource; label: string; color: string }[] = [ - { value: 'ALL', label: 'Alle', color: '#1e3a5f' }, - { value: 'VERIFIED_PORTFOLIO', label: 'Verified Portfolio', color: '#1e3a5f' }, - { value: 'EXTERNAL_MARKET', label: 'Marktinserate', color: '#d97706' }, - { value: 'FUTURE_AVAILABILITY', label: 'Zukunftssignale', color: '#7c3aed' }, -] - -const SORT_OPTIONS: { value: SortBy; label: string }[] = [ - { value: 'score', label: 'Relevanz' }, - { value: 'area', label: 'Fläche' }, - { value: 'rent', label: 'Mietpreis' }, -] - -export function ResultFilterBar({ filterSource, onFilterChange, sortBy, onSortChange }: Props) { - return ( - - - - {FILTER_OPTIONS.map(({ value, label, color }) => { - const active = filterSource === value - return ( - onFilterChange(value)} - sx={{ - bgcolor: active ? color : 'transparent', - color: active ? 'white' : 'text.secondary', - border: `1px solid ${active ? color : '#e2e8f0'}`, - fontWeight: active ? 600 : 400, - }} - /> - ) - })} - - - Sortierung: - {SORT_OPTIONS.map(({ value, label }) => ( - onSortChange(value)} - sx={{ - bgcolor: sortBy === value ? '#1e3a5f' : 'transparent', - color: sortBy === value ? 'white' : 'text.secondary', - border: `1px solid ${sortBy === value ? '#1e3a5f' : '#e2e8f0'}`, - }} - /> - ))} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/results/ResultTypeBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/results/ResultTypeBadge.tsx deleted file mode 100644 index dd35a98..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/results/ResultTypeBadge.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { ResultType } from '../../domain/enums' -import { Chip } from '@mui/material' - -const TYPE_META: Record = { - VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' }, - EXTERNAL_MARKET: { label: 'Marktinserat', color: '#d97706' }, - FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed' }, -} - -export function ResultTypeBadge({ resultType }: { resultType: ResultType }) { - const meta = TYPE_META[resultType] ?? { label: resultType, color: '#64748b' } - return ( - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/results/UnifiedResultCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/results/UnifiedResultCard.tsx deleted file mode 100644 index 537e982..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/results/UnifiedResultCard.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useNavigate } from 'react-router' -import { MatchCardCompact } from '../match-card/MatchCardCompact' -import { buildMatchCardViewModel } from '../../features/matching/matchCardAdapter' -import { useCompareStore } from '../../stores/compareStore' -import { useShortlistStore } from '../../stores/shortlistStore' -import type { UnifiedMatchResult } from '../../domain/unifiedResult' -import type { MatchCardAction } from '../match-card/MatchCardViewModel' - -interface Props { - result: UnifiedMatchResult -} - -function getResultTitle(result: UnifiedMatchResult): string { - if (result.resultType !== 'FUTURE_AVAILABILITY') { - return (result as any).property?.title ?? result.matchId - } - return (result as any).signal?.companyName ?? result.matchId -} - -export function UnifiedResultCard({ result }: Props) { - const navigate = useNavigate() - const { addToCompare, removeFromCompare, isInCompare, isFull } = useCompareStore() - const { openAddDialog } = useShortlistStore() - const inCompare = isInCompare(result.matchId) - - const actions: MatchCardAction[] = [ - { - id: 'shortlist', - label: 'Shortlist', - actionType: 'SAVE_SHORTLIST', - variant: 'secondary', - onClick: () => openAddDialog({ - resultId: result.matchId, - resultType: result.resultType, - title: getResultTitle(result), - matchScore: result.matchScore, - confidenceScore: result.match.confidenceLevel, - sourceLabel: result.resultType, - addedBy: 'admin@ideal-sharing.ch', - }), - }, - { - id: 'compare', - label: inCompare ? 'Im Vergleich' : 'Vergleichen', - actionType: 'ADD_COMPARE', - variant: inCompare ? 'primary' : 'secondary', - disabled: !inCompare && isFull(), - onClick: () => { - if (inCompare) removeFromCompare(result.matchId) - else addToCompare(result) - }, - }, - { - id: 'details', - label: 'Details', - actionType: 'OPEN_DETAIL', - variant: 'primary', - onClick: () => navigate(`/demand/results/${result.matchId}`), - }, - ] - - const vm = buildMatchCardViewModel(result, actions) - return -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/results/UnifiedResultFeed.tsx b/.claude/worktrees/agent-a82a3716/src/components/results/UnifiedResultFeed.tsx deleted file mode 100644 index 0aaad78..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/results/UnifiedResultFeed.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { UnifiedMatchResult } from '../../domain/unifiedResult' -import { UnifiedResultCard } from './UnifiedResultCard' - -interface Props { - results: UnifiedMatchResult[] -} - -export function UnifiedResultFeed({ results }: Props) { - return ( - <> - {results.map(result => ( - - ))} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/results/index.ts b/.claude/worktrees/agent-a82a3716/src/components/results/index.ts deleted file mode 100644 index 89456a5..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/results/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { ResultTypeBadge } from './ResultTypeBadge' -export { ResultConfidenceSummary } from './ResultConfidenceSummary' -export { FeedEmptyState } from './FeedEmptyState' -export { FeedSkeleton } from './FeedSkeleton' -export { ResultFeedHeader } from './ResultFeedHeader' -export { ResultFilterBar } from './ResultFilterBar' -export { UnifiedResultCard } from './UnifiedResultCard' -export { UnifiedResultFeed } from './UnifiedResultFeed' diff --git a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewActionToolbar.tsx b/.claude/worktrees/agent-a82a3716/src/components/review/ReviewActionToolbar.tsx deleted file mode 100644 index 9b4dd92..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewActionToolbar.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { Box, Button, CircularProgress } from '@mui/material' -import { CheckCircle, XCircle, AlertTriangle, ArrowUpCircle } from 'lucide-react' -import type { ReviewTask, ReviewTaskStatus } from '../../domain/review' -import type { UserRole } from '../../domain/enums' - -interface ReviewActionToolbarProps { - task: ReviewTask - userRole: UserRole - onAction: (status: ReviewTaskStatus, note?: string) => void - isSubmitting?: boolean -} - -export function ReviewActionToolbar({ task, userRole, onAction, isSubmitting }: ReviewActionToolbarProps) { - const isActive = task.status === 'PENDING' || task.status === 'IN_REVIEW' || task.status === 'ESCALATED' - const canApproveReject = isActive && (userRole === 'REVIEWER' || userRole === 'ORGANIZATION_ADMIN' || userRole === 'SUPER_ADMIN') - const canEscalate = task.status !== 'ESCALATED' && task.status !== 'APPROVED' && task.status !== 'REJECTED' - && (userRole === 'ORGANIZATION_ADMIN' || userRole === 'SUPER_ADMIN') - const canRequestMore = task.status !== 'NEEDS_MORE_DATA' && task.status !== 'APPROVED' && task.status !== 'REJECTED' - - if (!canApproveReject && !canEscalate && !canRequestMore) return null - - return ( - - {canApproveReject && ( - <> - - - - )} - {canRequestMore && ( - - )} - {canEscalate && ( - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewDetailPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/review/ReviewDetailPanel.tsx deleted file mode 100644 index 40a15e4..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewDetailPanel.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { Box, Divider, IconButton, LinearProgress, Typography } from '@mui/material' -import { X } from 'lucide-react' -import { ReviewEntityTypeBadge } from './ReviewEntityTypeBadge' -import { ReviewPriorityBadge } from './ReviewPriorityBadge' -import { ReviewStatusBadge } from './ReviewStatusBadge' -import { ReviewNotesPanel } from './ReviewNotesPanel' -import { ReviewActionToolbar } from './ReviewActionToolbar' -import type { ReviewTask, ReviewTaskStatus } from '../../domain/review' -import type { UserRole } from '../../domain/enums' - -interface ReviewDetailPanelProps { - task: ReviewTask - userRole: UserRole - onClose: () => void - onAction: (status: ReviewTaskStatus) => void - onAddNote: (content: string) => void - isSubmitting?: boolean -} - -const ENTITY_TYPE_LABELS: Record = { - FUTURE_SIGNAL: 'Zukunftssignal', - MATCH_EXPLANATION: 'Match-Begründung', - LOW_CONFIDENCE_MATCH: 'Niedr. Konfidenz-Match', - CONTACT_RELEASE: 'Kontaktfreigabe', - AI_OUTPUT: 'AI-Output', - PROPERTY_DATA_ISSUE: 'Datenfehler', -} - -const RISK_LABELS: Record = { - LOW: 'Niedrig', MEDIUM: 'Mittel', HIGH: 'Hoch', CRITICAL: 'Kritisch', -} - -const RISK_COLORS: Record = { - LOW: '#1a7a4a', MEDIUM: '#d97706', HIGH: '#ea580c', CRITICAL: '#c0392b', -} - -function MetaRow({ label, value }: { label: string; value: string | undefined }) { - if (!value) return null - return ( - - - {label} - - - {value} - - - ) -} - -export function ReviewDetailPanel({ - task, - userRole, - onClose, - onAction, - onAddNote, - isSubmitting, -}: ReviewDetailPanelProps) { - const isActive = task.status === 'PENDING' || task.status === 'IN_REVIEW' || task.status === 'ESCALATED' - const canAddNote = isActive || task.status === 'NEEDS_MORE_DATA' - - return ( - - {/* Header */} - - - - - - - - - - {task.title} - - - - - - - - - {/* Scrollable body */} - - {/* Description */} - {task.description && ( - - {task.description} - - )} - - {/* Metadata */} - - - - - - {task.relatedOrganizationId && ( - - )} - {task.promptVersion && ( - - )} - - - {/* Confidence / Risk context */} - {(task.confidenceScore !== undefined || task.riskLevel) && ( - <> - - {task.confidenceScore !== undefined && ( - - - Konfidenz - - {Math.round(task.confidenceScore * 100)}% - - - = 0.7 ? '#1a7a4a' : task.confidenceScore >= 0.5 ? '#d97706' : '#c0392b', - }, - }} - /> - - )} - {task.riskLevel && ( - - Risikostufe - - {RISK_LABELS[task.riskLevel] ?? task.riskLevel} - - - )} - {task.matchScore !== undefined && ( - - Match-Score - {task.matchScore}% - - )} - - )} - - - - {/* Actions */} - - - - - - - {/* Notes */} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewEmptyState.tsx b/.claude/worktrees/agent-a82a3716/src/components/review/ReviewEmptyState.tsx deleted file mode 100644 index ff49e06..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewEmptyState.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Box, Typography } from '@mui/material' -import { CheckCircle, MousePointer, ShieldOff } from 'lucide-react' - -interface ReviewEmptyStateProps { - variant: 'empty-queue' | 'no-selection' | 'no-permission' -} - -const CONFIG = { - 'empty-queue': { - icon: CheckCircle, - color: '#1a7a4a', - title: 'Keine ausstehenden Reviews', - desc: 'Alle Aufgaben wurden bearbeitet. Gute Arbeit.', - }, - 'no-selection': { - icon: MousePointer, - color: '#94a3b8', - title: 'Aufgabe wählen', - desc: 'Klicken Sie auf eine Aufgabe in der Liste, um Details und Aktionen anzuzeigen.', - }, - 'no-permission': { - icon: ShieldOff, - color: '#c0392b', - title: 'Kein Zugriff', - desc: 'Sie haben keine Berechtigung, Review-Aufgaben zu bearbeiten.', - }, -} - -export function ReviewEmptyState({ variant }: ReviewEmptyStateProps) { - const { icon: Icon, color, title, desc } = CONFIG[variant] - return ( - - - {title} - {desc} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewEntityTypeBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/review/ReviewEntityTypeBadge.tsx deleted file mode 100644 index 8b320d5..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewEntityTypeBadge.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Chip } from '@mui/material' -import type { ReviewEntityType } from '../../domain/review' - -interface ReviewEntityTypeBadgeProps { - entityType: ReviewEntityType - size?: 'small' | 'medium' -} - -const CONFIG: Record = { - FUTURE_SIGNAL: { label: 'Zukunftssignal', color: '#7c3aed' }, - MATCH_EXPLANATION: { label: 'Match-Begründung', color: '#1e3a5f' }, - LOW_CONFIDENCE_MATCH: { label: 'Niedr. Konfidenz', color: '#ea580c' }, - CONTACT_RELEASE: { label: 'Kontaktfreigabe', color: '#0891b2' }, - AI_OUTPUT: { label: 'AI-Output', color: '#4f46e5' }, - PROPERTY_DATA_ISSUE: { label: 'Datenfehler', color: '#c0392b' }, -} - -export function ReviewEntityTypeBadge({ entityType, size = 'small' }: ReviewEntityTypeBadgeProps) { - const { label, color } = CONFIG[entityType] ?? { label: entityType, color: '#64748b' } - return ( - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewFilterBar.tsx b/.claude/worktrees/agent-a82a3716/src/components/review/ReviewFilterBar.tsx deleted file mode 100644 index 80a430e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewFilterBar.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Box, MenuItem, Select, Typography } from '@mui/material' -import { ReviewEntityType, ReviewPriority, ReviewTaskStatus } from '../../domain/review' -import type { ReviewFilters } from '../../provider/IReviewProvider' - -interface ReviewFilterBarProps { - filters: ReviewFilters - onChange: (f: ReviewFilters) => void - totalCount: number - filteredCount: number -} - -export function ReviewFilterBar({ filters, onChange, totalCount, filteredCount }: ReviewFilterBarProps) { - const activeCount = Object.values(filters).filter(Boolean).length - - return ( - - - - - - - - - {activeCount > 0 ? `${filteredCount} / ${totalCount}` : `${totalCount} Aufgaben`} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewNotesPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/review/ReviewNotesPanel.tsx deleted file mode 100644 index d72a036..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewNotesPanel.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { useState } from 'react' -import { Box, Button, TextField, Typography } from '@mui/material' -import { MessageSquare } from 'lucide-react' -import type { ReviewNote } from '../../domain/review' - -interface ReviewNotesPanelProps { - notes: ReviewNote[] - canAddNote: boolean - onAddNote: (content: string) => void - isSubmitting?: boolean -} - -export function ReviewNotesPanel({ notes, canAddNote, onAddNote, isSubmitting }: ReviewNotesPanelProps) { - const [noteText, setNoteText] = useState('') - - const handleSubmit = () => { - if (!noteText.trim()) return - onAddNote(noteText.trim()) - setNoteText('') - } - - return ( - - - - - Notizen ({notes.length}) - - - - {/* Existing notes */} - {notes.length > 0 ? ( - - {[...notes].reverse().map(note => ( - - - - {note.createdBy} - - - {new Date(note.createdAt).toLocaleString('de-CH', { dateStyle: 'short', timeStyle: 'short' })} - - - - {note.content} - - - ))} - - ) : ( - - Noch keine Notizen. - - )} - - {/* Add note */} - {canAddNote && ( - - setNoteText(e.target.value)} - sx={{ mb: 0.75, fontSize: '0.8125rem' }} - /> - - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewPriorityBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/review/ReviewPriorityBadge.tsx deleted file mode 100644 index 6732c33..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewPriorityBadge.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Chip } from '@mui/material' -import type { ReviewPriority } from '../../domain/review' - -interface ReviewPriorityBadgeProps { - priority: ReviewPriority - size?: 'small' | 'medium' -} - -const CONFIG: Record = { - LOW: { label: 'Niedrig', color: '#64748b' }, - MEDIUM: { label: 'Mittel', color: '#d97706' }, - HIGH: { label: 'Hoch', color: '#ea580c' }, - CRITICAL: { label: 'Kritisch', color: '#c0392b' }, -} - -export function ReviewPriorityBadge({ priority, size = 'small' }: ReviewPriorityBadgeProps) { - const { label, color } = CONFIG[priority] ?? CONFIG.MEDIUM - return ( - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewStatusBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/review/ReviewStatusBadge.tsx deleted file mode 100644 index 3f35414..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewStatusBadge.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Chip } from '@mui/material' -import type { ReviewTaskStatus } from '../../domain/review' - -interface ReviewStatusBadgeProps { - status: ReviewTaskStatus - size?: 'small' | 'medium' -} - -const CONFIG: Record = { - PENDING: { label: 'Ausstehend', color: '#d97706' }, - IN_REVIEW: { label: 'In Prüfung', color: '#2563eb' }, - APPROVED: { label: 'Genehmigt', color: '#1a7a4a' }, - REJECTED: { label: 'Abgelehnt', color: '#c0392b' }, - NEEDS_MORE_DATA: { label: 'Mehr Daten nötig', color: '#7c3aed' }, - ESCALATED: { label: 'Eskaliert', color: '#ea580c' }, -} - -export function ReviewStatusBadge({ status, size = 'small' }: ReviewStatusBadgeProps) { - const { label, color } = CONFIG[status] ?? CONFIG.PENDING - return ( - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewTaskCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/review/ReviewTaskCard.tsx deleted file mode 100644 index 8e5d39b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/review/ReviewTaskCard.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { Box, LinearProgress, Typography } from '@mui/material' -import { Calendar, User } from 'lucide-react' -import { ReviewStatusBadge } from './ReviewStatusBadge' -import { ReviewPriorityBadge } from './ReviewPriorityBadge' -import { ReviewEntityTypeBadge } from './ReviewEntityTypeBadge' -import type { ReviewTask } from '../../domain/review' - -interface ReviewTaskCardProps { - task: ReviewTask - isSelected: boolean - onSelect: (task: ReviewTask) => void -} - -function isOverdue(dueDate?: string): boolean { - if (!dueDate) return false - return new Date(dueDate) < new Date() -} - -export function ReviewTaskCard({ task, isSelected, onSelect }: ReviewTaskCardProps) { - const overdue = isOverdue(task.dueDate) - const isActive = task.status === 'PENDING' || task.status === 'IN_REVIEW' || task.status === 'ESCALATED' - - return ( - onSelect(task)} - sx={{ - px: 1.5, - py: 1.25, - cursor: 'pointer', - borderBottom: '1px solid #f1f5f9', - borderLeft: isSelected - ? '3px solid #1e3a5f' - : overdue && isActive - ? '3px solid #c0392b' - : task.priority === 'CRITICAL' && isActive - ? '3px solid #ea580c' - : '3px solid transparent', - bgcolor: isSelected ? '#eff6ff' : 'white', - '&:hover': { bgcolor: isSelected ? '#eff6ff' : '#f8fafc' }, - transition: 'background-color 0.1s ease', - }} - > - {/* Badges row */} - - - - - - - {/* Title */} - - {task.title} - - - {/* Description */} - {task.description && ( - - {task.description} - - )} - - {/* Confidence bar */} - {task.confidenceScore !== undefined && ( - - = 0.7 ? '#1a7a4a' : task.confidenceScore >= 0.5 ? '#d97706' : '#c0392b', - }, - }} - /> - - )} - - {/* Footer: meta */} - - {task.assignedTo && ( - - - - {task.assignedTo} - - - )} - {task.dueDate && ( - - - - {new Date(task.dueDate).toLocaleDateString('de-CH')} - - - )} - - {new Date(task.createdAt).toLocaleDateString('de-CH')} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/review/index.ts b/.claude/worktrees/agent-a82a3716/src/components/review/index.ts deleted file mode 100644 index 2cee22d..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/review/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { ReviewStatusBadge } from './ReviewStatusBadge' -export { ReviewPriorityBadge } from './ReviewPriorityBadge' -export { ReviewEntityTypeBadge } from './ReviewEntityTypeBadge' -export { ReviewEmptyState } from './ReviewEmptyState' -export { ReviewFilterBar } from './ReviewFilterBar' -export { ReviewTaskCard } from './ReviewTaskCard' -export { ReviewNotesPanel } from './ReviewNotesPanel' -export { ReviewActionToolbar } from './ReviewActionToolbar' -export { ReviewDetailPanel } from './ReviewDetailPanel' diff --git a/.claude/worktrees/agent-a82a3716/src/components/scores/ConfidenceScore.tsx b/.claude/worktrees/agent-a82a3716/src/components/scores/ConfidenceScore.tsx deleted file mode 100644 index eeb885e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/scores/ConfidenceScore.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Box, Typography } from '@mui/material' -import { confidenceHex } from '../../lib/utils' -import { scoreToConfidenceLevel } from '../../lib/ds' -import { ConfidenceBadge } from '../badges/ConfidenceBadge' - -interface ConfidenceScoreProps { - score: number - showLabel?: boolean - compact?: boolean -} - -export function ConfidenceScore({ score, showLabel = false, compact = false }: ConfidenceScoreProps) { - const color = confidenceHex(score) - const pct = `${Math.round(score * 100)}%` - - if (compact) { - return ( - - {pct} - - ) - } - - return ( - - - {pct} - - {showLabel && ( - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/scores/DataQualityScore.tsx b/.claude/worktrees/agent-a82a3716/src/components/scores/DataQualityScore.tsx deleted file mode 100644 index 68c523e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/scores/DataQualityScore.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Box, LinearProgress, Typography } from '@mui/material' -import { dataQualityHex } from '../../lib/utils' -import { scoreToDataQualityLevel } from '../../lib/ds' -import { DATA_QUALITY_LABELS } from '../../lib/constants' - -interface DataQualityScoreProps { - score: number - compact?: boolean -} - -export function DataQualityScore({ score, compact = false }: DataQualityScoreProps) { - const color = dataQualityHex(score) - const pct = Math.round(score * 100) - const level = scoreToDataQualityLevel(score) - const label = DATA_QUALITY_LABELS[level] ?? level - - if (compact) { - return ( - - {pct}% - - ) - } - - return ( - - - {label} - - {pct}% - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/scores/MatchScoreRing.tsx b/.claude/worktrees/agent-a82a3716/src/components/scores/MatchScoreRing.tsx deleted file mode 100644 index ebb74d0..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/scores/MatchScoreRing.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Box, Typography } from '@mui/material' -import { matchScoreColor } from '../../lib/utils' -import type { MatchStrength } from '../../domain/enums' -import { MATCH_STRENGTH_LABELS } from '../../lib/constants' - -interface MatchScoreRingProps { - score: number - strength: MatchStrength - size?: 'sm' | 'md' | 'lg' - showLabel?: boolean -} - -const SIZE_MAP = { - sm: { ring: 48, font: '0.875rem', label: '0.625rem' }, - md: { ring: 64, font: '1.125rem', label: '0.75rem' }, - lg: { ring: 80, font: '1.5rem', label: '0.8125rem' }, -} - -const COLOR_MAP = { - success: '#1a7a4a', - warning: '#d97706', - error: '#c0392b', -} - -export function MatchScoreRing({ score, strength, size = 'md', showLabel = true }: MatchScoreRingProps) { - const dim = SIZE_MAP[size] - const colorKey = matchScoreColor(score) - const color = COLOR_MAP[colorKey] - const bgColor = colorKey === 'success' ? 'rgba(26,122,74,0.08)' : colorKey === 'warning' ? 'rgba(217,119,6,0.08)' : 'rgba(192,57,43,0.08)' - - return ( - - - - {score} - - - {showLabel && ( - - {MATCH_STRENGTH_LABELS[strength]} - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/scores/ScoreBar.tsx b/.claude/worktrees/agent-a82a3716/src/components/scores/ScoreBar.tsx deleted file mode 100644 index f82523b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/scores/ScoreBar.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Box, LinearProgress, Typography } from '@mui/material' -import { matchScoreColor } from '../../lib/utils' - -const COLOR_MAP: Record = { - success: '#1a7a4a', - warning: '#d97706', - error: '#c0392b', -} - -interface ScoreBarProps { - label: string - value: number - weight?: number - color?: string - maxWidth?: number -} - -export function ScoreBar({ label, value, weight, color, maxWidth = 200 }: ScoreBarProps) { - const resolved = color ?? COLOR_MAP[matchScoreColor(value)] ?? '#1e3a5f' - - return ( - - - - {label} - - - {value} - - - - {weight !== undefined && ( - - Gewicht: {Math.round(weight * 100)}% - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/scores/ScoreBreakdownMini.tsx b/.claude/worktrees/agent-a82a3716/src/components/scores/ScoreBreakdownMini.tsx deleted file mode 100644 index 45f908b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/scores/ScoreBreakdownMini.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Box, Tooltip, Typography } from '@mui/material' -import { ScoreBar } from './ScoreBar' -import type { ScoreFactor } from '../../domain/match' - -interface ScoreBreakdownMiniProps { - factors: ScoreFactor[] - maxItems?: number - showContribution?: boolean -} - -export function ScoreBreakdownMini({ factors, maxItems = 5, showContribution = false }: ScoreBreakdownMiniProps) { - const visible = factors.slice(0, maxItems) - const overflow = factors.length - maxItems - - return ( - - {visible.map((f, i) => ( - - - - - - ))} - {overflow > 0 && ( - - +{overflow} weitere Faktoren - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/scores/index.ts b/.claude/worktrees/agent-a82a3716/src/components/scores/index.ts deleted file mode 100644 index 5617076..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/scores/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { MatchScoreRing } from './MatchScoreRing' -export { ConfidenceScore } from './ConfidenceScore' -export { DataQualityScore } from './DataQualityScore' -export { ScoreBar } from './ScoreBar' -export { ScoreBreakdownMini } from './ScoreBreakdownMini' diff --git a/.claude/worktrees/agent-a82a3716/src/components/shortlist/AddToShortlistDialog.tsx b/.claude/worktrees/agent-a82a3716/src/components/shortlist/AddToShortlistDialog.tsx deleted file mode 100644 index 8ea9258..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/shortlist/AddToShortlistDialog.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { useState } from 'react' -import { - Box, - Button, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - FormControlLabel, - Radio, - RadioGroup, - TextField, - Typography, -} from '@mui/material' -import { Plus } from 'lucide-react' -import { ShortlistStatusBadge } from './ShortlistStatusBadge' -import { useShortlists, useCreateShortlist, useAddToShortlist } from '../../hooks/useShortlists' -import { useShortlistStore } from '../../stores/shortlistStore' -import { useToastStore } from '../../stores/toastStore' -import { ShortlistStatus } from '../../domain/enums' - -export function AddToShortlistDialog() { - const { dialogOpen, pendingItem, closeAddDialog } = useShortlistStore() - const { data: shortlists = [], isLoading } = useShortlists() - const createShortlist = useCreateShortlist() - const addToShortlist = useAddToShortlist() - - const showToast = useToastStore((s) => s.showToast) - const [selectedId, setSelectedId] = useState('') - const [creatingNew, setCreatingNew] = useState(false) - const [newTitle, setNewTitle] = useState('') - const [note, setNote] = useState('') - - function handleClose() { - closeAddDialog() - setSelectedId('') - setCreatingNew(false) - setNewTitle('') - setNote('') - } - - async function handleConfirm() { - if (!pendingItem) return - const itemWithNote = { ...pendingItem, note: note.trim() || undefined } - - let targetId = selectedId - - if (creatingNew) { - if (!newTitle.trim()) return - const created = await createShortlist.mutateAsync({ - title: newTitle.trim(), - items: [], - status: ShortlistStatus.DRAFT, - createdBy: 'admin@ideal-sharing.ch', - organizationId: 'org-wincasa', - }) - targetId = created.data.id - } - - if (!targetId) return - - const prevShortlist = shortlists.find(s => s.id === targetId) - const alreadyExists = prevShortlist?.items.some(i => i.resultId === pendingItem.resultId) - - await addToShortlist.mutateAsync({ shortlistId: targetId, item: itemWithNote }) - - handleClose() - if (alreadyExists) { - showToast('Bereits in dieser Shortlist vorhanden.', 'warning') - } else { - showToast('Zur Shortlist hinzugefügt.') - } - } - - const isBusy = createShortlist.isPending || addToShortlist.isPending - const canConfirm = !isBusy && ((creatingNew && !!newTitle.trim()) || (!creatingNew && !!selectedId)) - - return ( - <> - - Zur Shortlist hinzufügen - - {pendingItem && ( - - {pendingItem.title} - - Score {pendingItem.matchScore} · {pendingItem.resultType} - - - )} - - {isLoading ? ( - - - - ) : ( - { - if (e.target.value === '__new__') { setCreatingNew(true); setSelectedId('') } - else { setCreatingNew(false); setSelectedId(e.target.value) } - }}> - {shortlists.map(sl => ( - } - label={ - - {sl.title} - - - } - /> - ))} - } - label={ - - - Neue Shortlist erstellen - - } - /> - - )} - - {creatingNew && ( - setNewTitle(e.target.value)} - sx={{ mt: 1.5 }} - /> - )} - - setNote(e.target.value)} - sx={{ mt: 2 }} - /> - - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/shortlist/DecisionBriefDraftPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/shortlist/DecisionBriefDraftPanel.tsx deleted file mode 100644 index b76c4f0..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/shortlist/DecisionBriefDraftPanel.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useState } from 'react' -import { Accordion, AccordionDetails, AccordionSummary, Alert, Box, Button, CircularProgress, Typography } from '@mui/material' -import { ChevronDown, Sparkles } from 'lucide-react' -import { aiService } from '../../services/aiService' -import type { DecisionBrief } from '../../services/aiService' - -interface Props { - shortlistId: string -} - -export function DecisionBriefDraftPanel({ shortlistId }: Props) { - const [brief, setBrief] = useState(null) - const [loading, setLoading] = useState(false) - - async function handleGenerate() { - setLoading(true) - try { - const resp = await aiService.generateDecisionBrief(shortlistId) - setBrief(resp.data) - } finally { - setLoading(false) - } - } - - return ( - - - KI Decision Brief - - - - {loading ? ( - - - Brief wird generiert… - - ) : brief ? ( - - } sx={{ mb: 2, py: 0.5 }}> - KI-ENTWURF - - Automatisch generiert — bitte prüfen und anpassen. - - - - {brief.summary} - - {brief.sections.map((section, i) => ( - - } sx={{ minHeight: 40, '& .MuiAccordionSummary-content': { my: 0.5 } }}> - {section.title} - - - {section.body} - - - ))} - - - - ) : ( - - - - Decision Brief - - KI analysiert Ihre Shortlist und erstellt einen strukturierten Entscheidungsentwurf. - - - - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistCard.tsx deleted file mode 100644 index 577d12e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistCard.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Box, Chip, Typography } from '@mui/material' -import { ShortlistStatusBadge } from './ShortlistStatusBadge' -import { useShortlistStore } from '../../stores/shortlistStore' -import type { Shortlist } from '../../domain/shortlist' - -interface Props { - shortlist: Shortlist -} - -export function ShortlistCard({ shortlist }: Props) { - const { selectedShortlistId, setSelectedShortlist } = useShortlistStore() - const isSelected = selectedShortlistId === shortlist.id - - const updatedDate = new Date(shortlist.updatedAt).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' }) - - return ( - setSelectedShortlist(isSelected ? null : shortlist.id)} - sx={{ - p: 1.5, - cursor: 'pointer', - borderBottom: '1px solid #f1f5f9', - borderLeft: isSelected ? '3px solid #1e3a5f' : '3px solid transparent', - bgcolor: isSelected ? '#eff6ff' : 'transparent', - '&:hover': { bgcolor: isSelected ? '#eff6ff' : '#f8fafc' }, - }} - > - - - {shortlist.title} - - - - - - {updatedDate} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistDetail.tsx b/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistDetail.tsx deleted file mode 100644 index 9cbacfe..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistDetail.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { useState } from 'react' -import { Alert, Box, Button, Chip, TextField, Typography } from '@mui/material' -import { useNavigate } from 'react-router' -import { CheckCircle, Columns2, Pencil } from 'lucide-react' -import { ShortlistStatusBadge } from './ShortlistStatusBadge' -import { ShortlistItemCard } from './ShortlistItemCard' -import { ShortlistEmptyState } from './ShortlistEmptyState' -import { useUpdateShortlist } from '../../hooks/useShortlists' -import { useCompareStore } from '../../stores/compareStore' -import { useToastStore } from '../../stores/toastStore' -import { ShortlistStatus } from '../../domain/enums' -import type { Shortlist } from '../../domain/shortlist' - -interface Props { - shortlist: Shortlist -} - -export function ShortlistDetail({ shortlist }: Props) { - const navigate = useNavigate() - const updateShortlist = useUpdateShortlist() - const { addToCompare, compareItems } = useCompareStore() - const showToast = useToastStore((s) => s.showToast) - - const [editingTitle, setEditingTitle] = useState(false) - const [titleDraft, setTitleDraft] = useState(shortlist.title) - - const isFinalized = shortlist.status === ShortlistStatus.FINALIZED - const isDraft = shortlist.status === ShortlistStatus.DRAFT - const isReviewReady = shortlist.status === ShortlistStatus.REVIEW_READY - - function handleTitleSave() { - if (titleDraft.trim() && titleDraft.trim() !== shortlist.title) { - updateShortlist.mutate( - { id: shortlist.id, data: { title: titleDraft.trim() } }, - { - onSuccess: () => showToast('Titel gespeichert.'), - onError: () => showToast('Titel konnte nicht gespeichert werden.', 'error'), - } - ) - } - setEditingTitle(false) - } - - function handleMarkReviewReady() { - updateShortlist.mutate( - { id: shortlist.id, data: { status: ShortlistStatus.REVIEW_READY } }, - { - onSuccess: () => showToast('Shortlist als prüfbereit markiert.'), - onError: () => showToast('Statusänderung fehlgeschlagen.', 'error'), - } - ) - } - - function handleFinalize() { - updateShortlist.mutate( - { id: shortlist.id, data: { status: ShortlistStatus.FINALIZED } }, - { - onSuccess: () => showToast('Shortlist finalisiert.'), - onError: () => showToast('Finalisierung fehlgeschlagen.', 'error'), - } - ) - } - - function handleOpenCompare() { - for (const item of shortlist.items) { - if (item.resultType === 'FUTURE_AVAILABILITY') continue - // Build a minimal UnifiedMatchResult-compatible object for compare - const fakeResult = { - matchId: item.resultId, - needId: '', - matchScore: item.matchScore, - resultType: item.resultType as 'VERIFIED_PORTFOLIO' | 'EXTERNAL_MARKET', - property: { id: item.propertyId ?? item.resultId, title: item.title } as any, - match: { id: item.resultId, matchScore: item.matchScore, confidenceLevel: item.confidenceScore ?? 0.8 } as any, - } - if (compareItems.length < 4) addToCompare(fakeResult as any) - } - navigate('/demand/compare') - } - - const updatedDate = new Date(shortlist.updatedAt).toLocaleDateString('de-CH', { - day: '2-digit', month: '2-digit', year: 'numeric', - }) - - return ( - - {/* Header */} - - - {editingTitle ? ( - setTitleDraft(e.target.value)} - onBlur={handleTitleSave} - onKeyDown={e => { if (e.key === 'Enter') handleTitleSave(); if (e.key === 'Escape') { setTitleDraft(shortlist.title); setEditingTitle(false) } }} - sx={{ flex: 1, '& .MuiInputBase-input': { fontSize: '1.1rem', fontWeight: 700 } }} - /> - ) : ( - - {shortlist.title} - {!isFinalized && ( - - )} - - )} - - - - - - {shortlist.items.length} Objekte - - - Erstellt von {shortlist.createdBy} - - {shortlist.organizationId && ( - - {shortlist.organizationId} - - )} - - Aktualisiert {updatedDate} - - - - {isFinalized && ( - } sx={{ mb: 1.5, py: 0.25 }}> - Diese Shortlist ist finalisiert — keine Änderungen möglich. - - )} - - - {isDraft && ( - - )} - {isReviewReady && ( - - )} - {shortlist.items.length > 0 && ( - - )} - {shortlist.description && ( - - )} - - - - {/* Items */} - - {shortlist.items.length === 0 - ? - : shortlist.items.map(item => ( - - )) - } - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistEmptyState.tsx b/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistEmptyState.tsx deleted file mode 100644 index 70a242b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistEmptyState.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Box, Typography } from '@mui/material' -import { Bookmark, BookmarkPlus } from 'lucide-react' - -type EmptyContext = 'no-shortlists' | 'empty-shortlist' - -const META: Record = { - 'no-shortlists': { - icon: , - title: 'Noch keine Shortlists', - desc: 'Speichern Sie Suchergebnisse in einer Shortlist, um sie gezielt zu vergleichen und zu entscheiden.', - }, - 'empty-shortlist': { - icon: , - title: 'Shortlist ist leer', - desc: 'Fügen Sie Objekte aus den Suchergebnissen oder dem Match-Feed hinzu.', - }, -} - -export function ShortlistEmptyState({ context }: { context: EmptyContext }) { - const { icon, title, desc } = META[context] - return ( - - {icon} - {title} - {desc} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistItemCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistItemCard.tsx deleted file mode 100644 index 6c5e994..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistItemCard.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Box, Chip, IconButton, Typography } from '@mui/material' -import { X } from 'lucide-react' -import { useRemoveFromShortlist } from '../../hooks/useShortlists' -import { useToastStore } from '../../stores/toastStore' -import type { ShortlistItem } from '../../domain/shortlist' - -const RESULT_TYPE_LABEL: Record = { - VERIFIED_PORTFOLIO: 'Portfolio', - EXTERNAL_MARKET: 'Markt', - FUTURE_AVAILABILITY: 'Signal', -} - -const SCORE_COLOR = (s: number) => s >= 75 ? '#1a7a4a' : s >= 55 ? '#d97706' : '#c0392b' - -interface Props { - item: ShortlistItem - shortlistId: string - isFinalized: boolean -} - -export function ShortlistItemCard({ item, shortlistId, isFinalized }: Props) { - const removeItem = useRemoveFromShortlist() - const showToast = useToastStore((s) => s.showToast) - - const addedDate = new Date(item.addedAt).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' }) - - return ( - - - - {item.title} - - - - - {item.sourceLabel ?? item.resultType} · {item.addedBy} · {addedDate} - - {item.note && ( - - {item.note} - - )} - - {!isFinalized && ( - removeItem.mutate( - { shortlistId, resultId: item.resultId }, - { - onSuccess: () => showToast('Objekt aus Shortlist entfernt.'), - onError: () => showToast('Entfernen fehlgeschlagen.', 'error'), - } - )} - sx={{ color: '#94a3b8', '&:hover': { color: '#c0392b' }, flexShrink: 0 }} - > - - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistList.tsx b/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistList.tsx deleted file mode 100644 index 0251c7a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistList.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useState } from 'react' -import { Box, Button, CircularProgress, Skeleton, TextField } from '@mui/material' -import { Plus } from 'lucide-react' -import { ShortlistCard } from './ShortlistCard' -import { ShortlistEmptyState } from './ShortlistEmptyState' -import { useCreateShortlist } from '../../hooks/useShortlists' -import { useShortlistStore } from '../../stores/shortlistStore' -import { ShortlistStatus } from '../../domain/enums' -import type { Shortlist } from '../../domain/shortlist' - -interface Props { - shortlists: Shortlist[] - isLoading: boolean -} - -export function ShortlistList({ shortlists, isLoading }: Props) { - const [creating, setCreating] = useState(false) - const [newTitle, setNewTitle] = useState('') - const createShortlist = useCreateShortlist() - const { setSelectedShortlist } = useShortlistStore() - - async function handleCreate() { - if (!newTitle.trim()) return - const result = await createShortlist.mutateAsync({ - title: newTitle.trim(), - items: [], - status: ShortlistStatus.DRAFT, - createdBy: 'admin@ideal-sharing.ch', - organizationId: 'org-wincasa', - }) - setNewTitle('') - setCreating(false) - setSelectedShortlist(result.data.id) - } - - if (isLoading) { - return ( - - {[0, 1, 2].map(i => ( - - - - - ))} - - ) - } - - return ( - - - {shortlists.length === 0 && !creating - ? - : shortlists.map(sl => ) - } - - - - {creating ? ( - - setNewTitle(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleCreate(); if (e.key === 'Escape') { setCreating(false); setNewTitle('') } }} - sx={{ flex: 1, '& .MuiInputBase-input': { fontSize: 13 } }} - /> - {createShortlist.isPending - ? - : ( - - ) - } - - ) : ( - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistStatusBadge.tsx b/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistStatusBadge.tsx deleted file mode 100644 index e7025dc..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/shortlist/ShortlistStatusBadge.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Chip } from '@mui/material' -import type { ShortlistStatus } from '../../domain/enums' - -const STATUS_META: Record = { - DRAFT: { label: 'Entwurf', color: '#64748b' }, - REVIEW_READY: { label: 'Prüfbereit', color: '#d97706' }, - FINALIZED: { label: 'Finalisiert', color: '#1a7a4a' }, - ARCHIVED: { label: 'Archiviert', color: '#475569' }, - ACTIVE: { label: 'Aktiv', color: '#1e3a5f' }, - SHARED: { label: 'Geteilt', color: '#7c3aed' }, - CONVERTED: { label: 'Konvertiert', color: '#0891b2' }, -} - -export function ShortlistStatusBadge({ status }: { status: ShortlistStatus }) { - const meta = STATUS_META[status] ?? { label: status, color: '#64748b' } - return ( - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/shortlist/index.ts b/.claude/worktrees/agent-a82a3716/src/components/shortlist/index.ts deleted file mode 100644 index 39b78f9..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/shortlist/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { ShortlistStatusBadge } from './ShortlistStatusBadge' -export { ShortlistEmptyState } from './ShortlistEmptyState' -export { ShortlistItemCard } from './ShortlistItemCard' -export { ShortlistCard } from './ShortlistCard' -export { ShortlistList } from './ShortlistList' -export { ShortlistDetail } from './ShortlistDetail' -export { DecisionBriefDraftPanel } from './DecisionBriefDraftPanel' -export { AddToShortlistDialog } from './AddToShortlistDialog' diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/DashboardHeader.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/DashboardHeader.tsx deleted file mode 100644 index f7c0a9e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/DashboardHeader.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Box, Button, Chip, Typography } from '@mui/material' -import { useNavigate } from 'react-router' - -interface DashboardHeaderProps { - orgName?: string - lastUpdated?: string -} - -function formatLastUpdated(iso: string): string { - try { - return new Intl.DateTimeFormat('de-CH', { - dateStyle: 'short', - timeStyle: 'short', - }).format(new Date(iso)) - } catch { - return iso - } -} - -export function DashboardHeader({ orgName = 'Demo Organisation', lastUpdated }: DashboardHeaderProps) { - const navigate = useNavigate() - - return ( - - - - - {orgName} - - - - {lastUpdated && ( - - Zuletzt aktualisiert: {formatLastUpdated(lastUpdated)} - - )} - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/DashboardSkeleton.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/DashboardSkeleton.tsx deleted file mode 100644 index b9d45a3..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/DashboardSkeleton.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Box, Card, CardContent, Skeleton } from '@mui/material' - -function SkeletonCard() { - return ( - - - - - - - ) -} - -function SkeletonLargeCard() { - return ( - - - - - - - - ) -} - -export function DashboardSkeleton() { - return ( - - {/* Header skeleton */} - - - - - - {/* KPI grid skeleton */} - - {Array.from({ length: 6 }).map((_, i) => ( - - ))} - - - {/* Row 2 */} - - - - - - {/* Row 3 */} - - - - - - {/* Quick actions skeleton */} - - - - - {Array.from({ length: 6 }).map((_, i) => ( - - ))} - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/DataQualityWidget.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/DataQualityWidget.tsx deleted file mode 100644 index 04bdc38..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/DataQualityWidget.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Box, Button, Card, CardContent, Chip, LinearProgress, Typography } from '@mui/material' -import type { DataQualitySummary } from '../../domain/dashboard' - -interface DataQualityWidgetProps { - summary: DataQualitySummary - onNavigate: () => void -} - -function qualityColor(score: number): 'success' | 'warning' | 'error' { - if (score >= 80) return 'success' - if (score >= 60) return 'warning' - return 'error' -} - -export function DataQualityWidget({ summary, onNavigate }: DataQualityWidgetProps) { - const color = qualityColor(summary.avgScore) - - return ( - - - - - Datenqualität - - - - - - - - Ø Score - - - {summary.avgScore}% - - - - - - - - 0 ? 'error.main' : 'text.primary' }}> - {summary.critical} - - - Kritische Objekte - - - - - {summary.propertiesWithMissingCritical} - - - Fehlende Pflichtfelder - - - - - {summary.topMissingFields.length > 0 && ( - - - Häufig fehlende Felder: - - - {summary.topMissingFields.map(field => ( - - ))} - - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/FutureSignalWidget.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/FutureSignalWidget.tsx deleted file mode 100644 index 3cf9daa..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/FutureSignalWidget.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { Box, Card, CardContent, Divider, LinearProgress, Typography } from '@mui/material' -import { useNavigate } from 'react-router' -import type { FutureSignalSummary } from '../../domain/dashboard' - -interface FutureSignalWidgetProps { - summary: FutureSignalSummary -} - -interface StatItemProps { - label: string - value: number | string - highlight?: boolean -} - -function StatItem({ label, value, highlight }: StatItemProps) { - return ( - - - {value} - - - {label} - - - ) -} - -export function FutureSignalWidget({ summary }: FutureSignalWidgetProps) { - const navigate = useNavigate() - const total = summary.total || 1 - const dist = summary.timeHorizonDistribution - - return ( - - - - - Marktsignale - - navigate('/supply/future-availability')} - > - Alle anzeigen - - - - - - - - - - - - - {/* Time horizon distribution */} - - Zeithorizont-Verteilung - - - {[ - { label: '0–6 Monate', count: dist.short }, - { label: '6–12 Monate', count: dist.medium }, - { label: '12–24 Monate', count: dist.long }, - ].map(({ label, count }) => ( - - - {label} - {count} - - - - ))} - - - {summary.restricted > 0 && ( - <> - - - {summary.restricted} vertrauliche Signale — nur für berechtigte Nutzer sichtbar. - - - )} - - - - Marktsignale basieren auf AI-Analyse öffentlicher und interner Daten. Es handelt - sich um probabilistische Einschätzungen, keine bestätigten Objekte. - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/KpiCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/KpiCard.tsx deleted file mode 100644 index f90ecd6..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/KpiCard.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Card, CardContent, CardActionArea, Chip, Tooltip, Typography } from '@mui/material' -import type { KpiCardData } from '../../domain/dashboard' - -interface KpiCardProps { - card: KpiCardData -} - -export function KpiCard({ card }: KpiCardProps) { - const trendColor = - card.trend === 'up' ? 'success' : card.trend === 'down' ? 'error' : 'default' - - const content = ( - - - {card.label} - - - {card.value} - - {card.trendLabel && ( - - )} - - ) - - const card_ = ( - - {card.onClick ? ( - - {content} - - ) : ( - content - )} - - ) - - if (card.tooltip) { - return ( - - {card_} - - ) - } - - return card_ -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/KpiGrid.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/KpiGrid.tsx deleted file mode 100644 index 9c9a1e2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/KpiGrid.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Box } from '@mui/material' -import { useNavigate } from 'react-router' -import type { DashboardData, KpiCardData } from '../../domain/dashboard' -import { KpiCard } from './KpiCard' - -interface KpiGridProps { - data: DashboardData -} - -export function KpiGrid({ data }: KpiGridProps) { - const navigate = useNavigate() - const pendingCount = data.reviewTasks?.filter(t => t.status === 'PENDING').length ?? 0 - - const qualityColor = - data.avgDataQuality >= 80 - ? '#1a7a4a' - : data.avgDataQuality >= 60 - ? '#d97706' - : '#c0392b' - - const cards: KpiCardData[] = [ - { - id: 'active-properties', - label: 'Aktive Objekte', - value: `${data.activeProperties} / ${data.totalProperties}`, - tooltip: 'Objekte mit Status "Verfügbar jetzt" oder "Verfügbar bald"', - onClick: () => navigate('/supply/properties'), - }, - { - id: 'strong-matches', - label: 'Starke Matches', - value: data.strongMatchCount, - accent: '#1a7a4a', - tooltip: 'Matches mit einem Match-Score ≥ 80', - onClick: () => navigate('/supply/match-center'), - }, - { - id: 'avg-quality', - label: 'Ø Datenqualität', - value: `${data.avgDataQuality}%`, - accent: qualityColor, - tooltip: 'Durchschnittlicher Datenqualitäts-Score aller Objekte', - onClick: () => navigate('/supply/data-quality'), - }, - { - id: 'market-signals', - label: 'Marktsignale', - value: data.futureSignals?.total ?? '–', - tooltip: 'Erkannte Marktsignale und Future Availability Indikatoren', - onClick: () => navigate('/supply/future-availability'), - }, - { - id: 'review-pending', - label: 'Review ausstehend', - value: pendingCount, - accent: pendingCount > 0 ? '#d97706' : undefined, - tooltip: 'Matches und Signale, die eine manuelle Prüfung erfordern', - onClick: () => navigate('/ops/review-queue'), - }, - { - id: 'critical-gaps', - label: 'Kritische Datenlücken', - value: data.dataQuality?.critical ?? '–', - accent: (data.dataQuality?.critical ?? 0) > 0 ? '#c0392b' : undefined, - tooltip: 'Objekte mit Datenqualitäts-Score unter 50%', - onClick: () => navigate('/supply/data-quality'), - }, - ] - - return ( - - {cards.map(card => ( - - ))} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/NegotiationInsightsPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/NegotiationInsightsPanel.tsx deleted file mode 100644 index c8d9a06..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/NegotiationInsightsPanel.tsx +++ /dev/null @@ -1,345 +0,0 @@ -import { Box, Chip, Divider, LinearProgress, Paper, Typography } from '@mui/material' -import { CheckCircle, TrendingDown, TrendingUp } from 'lucide-react' -import { useProperties } from '../../hooks/useProperties' -import { useNeeds } from '../../hooks/useNeeds' -import { getCityIntelligence, getMarketRent } from '../../lib/locationIntelligence' -import type { Property } from '../../domain/property' - -// ── Selling argument generator ──────────────────────────────────────────────── - -interface Argument { - title: string - detail: string - strength: 'strong' | 'medium' -} - -function generateSellingArguments(p: Property): Argument[] { - const args: Argument[] = [] - const sf = p.softFactors - const intel = getCityIntelligence(p.location.city) - - if (sf) { - const footfall = sf.footfallScore ?? (sf.passerbyFrequency ? { LOW: 0.25, MEDIUM: 0.5, HIGH: 0.78, VERY_HIGH: 0.95 }[sf.passerbyFrequency] ?? 0 : 0) - if (footfall >= 0.72) - args.push({ title: 'Hervorragende Frequenzlage', detail: 'Überdurchschnittliche Passantenfrequenz — sichert Sichtbarkeit und Kundenzugang.', strength: 'strong' }) - - const taxScore = sf.taxEnvironmentScore ?? (intel ? Math.max(0, 1 - intel.taxIndexCanton / 150) : undefined) - if (taxScore !== undefined && taxScore >= 0.65) - args.push({ title: 'Steuerattraktiver Standort', detail: `${p.location.city} bietet eine günstige Steuerlast${intel ? ` (Index ${intel.taxIndexCanton}, CH = 100)` : ''} — relevant für Unternehmensansiedlungen.`, strength: 'strong' }) - - const ov = sf.commuterAccessScore - if (ov !== undefined && ov >= 0.72) - args.push({ title: 'Sehr gute ÖV-Anbindung', detail: sf.publicTransportMinutes ? `Ca. ${sf.publicTransportMinutes} Min. zum nächsten Bahnhof.` : 'Ausgezeichnete öffentliche Erreichbarkeit.', strength: 'strong' }) - - const prestige = sf.prestigeScore ?? (typeof sf.prestige === 'number' ? sf.prestige : undefined) - if (prestige !== undefined && prestige >= 0.7) - args.push({ title: 'Repräsentativer Standort', detail: 'Hoher Prestige-Wert — ideal für Unternehmen mit Repräsentationsanspruch und Aussenauftritt.', strength: 'strong' }) - - const talent = sf.talentAccessScore ?? (typeof sf.talentAccess === 'number' ? sf.talentAccess : undefined) - if (talent !== undefined && talent >= 0.65) - args.push({ title: 'Grosser Talentpool', detail: 'Zugang zu gut ausgebildeten Fachkräften im Einzugsgebiet — entscheidend für wachsende Unternehmen.', strength: 'medium' }) - - if (sf.flexibilityScore !== undefined && sf.flexibilityScore >= 0.65) - args.push({ title: 'Flexible Flächengestaltung', detail: 'Grundriss und Ausbaustandard ermöglichen individuelle Anpassungen.', strength: 'medium' }) - - if (sf.esgScore !== undefined && sf.esgScore >= 0.7) - args.push({ title: 'Nachhaltigkeitszertifizierung', detail: 'Guter ESG-Score — relevant für Unternehmen mit Nachhaltigkeitszielen und ESG-Reporting.', strength: 'medium' }) - } - - if (p.hardFacts?.isBarrierFree) - args.push({ title: 'Barrierefrei', detail: 'Vollständig rollstuhlgängig — gesetzlich zunehmend gefordert.', strength: 'medium' }) - - if (p.hardFacts?.parking && p.hardFacts.parking > 0) - args.push({ title: `${p.hardFacts.parking} Parkplätze inkl.`, detail: 'Eigene Parkierungsmöglichkeiten — in Städten ein knappes Gut.', strength: 'medium' }) - - if (p.hardFacts?.hasServerRoom) - args.push({ title: 'Serverraum vorhanden', detail: 'Sofortig nutzbare IT-Infrastruktur — spart Einrichtungskosten.', strength: 'medium' }) - - if (intel?.demandStrength === 'VERY_HIGH' || intel?.demandStrength === 'HIGH') - args.push({ title: 'Stark nachgefragter Markt', detail: `${p.location.city} verzeichnet ${intel.demandStrength === 'VERY_HIGH' ? 'sehr hohe' : 'hohe'} Nachfrage — kurze Leerstandszeiten zu erwarten.`, strength: 'strong' }) - - return args -} - -// ── Proactive weakness acknowledgement ─────────────────────────────────────── - -interface Weakness { - issue: string - mitigation: string -} - -function generateWeaknesses(p: Property): Weakness[] { - const ws: Weakness[] = [] - const sf = p.softFactors - const intel = getCityIntelligence(p.location.city) - - if (intel && intel.vacancyRatePct >= 5) - ws.push({ issue: 'Hohe Leerstandsquote in der Region', mitigation: 'Mietfreie Zeit oder Ausbaukostenbeteiligung als Anreiz anbieten.' }) - - if (intel && intel.taxIndexCanton >= 115) - ws.push({ issue: 'Überdurchschnittliche Steuerlast', mitigation: 'Andere Standortvorteile (Prestige, ÖV) gezielt hervorheben.' }) - - if (sf?.commuterAccessScore !== undefined && sf.commuterAccessScore < 0.45) - ws.push({ issue: 'Eingeschränkte ÖV-Anbindung', mitigation: 'Parkplatz-Angebot und Veloinfrastruktur als Alternative betonen.' }) - - if (p.hardFacts?.parking === 0 || (p.hardFacts?.parking === undefined && !sf?.parkingSpots)) - ws.push({ issue: 'Keine eigenen Parkplätze', mitigation: 'Öffentliche Parkhäuser in der Nähe aufzeigen. Ggf. Parkabonnement als Mietbonus anbieten.' }) - - if (p.dataQuality.score < 0.65) - ws.push({ issue: 'Unvollständige Objektdaten', mitigation: 'Fehlende Angaben vor dem Gespräch vervollständigen, um Vertrauen zu stärken.' }) - - return ws -} - -// ── Main component ──────────────────────────────────────────────────────────── - -interface Props { - property: Property -} - -export function NegotiationInsightsPanel({ property }: Props) { - const { data: allProperties = [] } = useProperties() - const { data: needs = [] } = useNeeds() - - const intel = getCityIntelligence(property.location.city) - const marketRent = getMarketRent(property.location.city, property.assetType) - const priceDiff = marketRent ? ((property.rentPricePerSqm - marketRent) / marketRent) * 100 : null - - // Comparable properties for price positioning - const comparables = allProperties - .filter(p => p.id !== property.id && p.assetType === property.assetType && p.location.city === property.location.city) - - const avgComparableRent = comparables.length > 0 - ? comparables.reduce((s, p) => s + p.rentPricePerSqm, 0) / comparables.length - : null - - // Active needs matching this property type/location - const matchingNeeds = needs.filter(n => - n.assetType === property.assetType && - (n.preferredLocations?.some(loc => loc.toLowerCase().includes(property.location.city.toLowerCase())) ?? false) - ) - - const sellingArgs = generateSellingArguments(property) - const weaknesses = generateWeaknesses(property) - const strongArgs = sellingArgs.filter(a => a.strength === 'strong') - const mediumArgs = sellingArgs.filter(a => a.strength === 'medium') - - return ( - - - {/* ── Price positioning ── */} - - Preispositionierung - - - - Ihr Preis - - CHF {property.rentPricePerSqm}/m² - - - {marketRent && ( - - Marktmedian {property.location.city} - - CHF {marketRent}/m² - - - )} - {avgComparableRent && ( - - Vergleichsangebote Ø - - CHF {Math.round(avgComparableRent)}/m² - - - )} - - - {priceDiff !== null && ( - 0 ? '#fff8f0' : '#f0fdf4', borderRadius: 1.5 }}> - {priceDiff > 0 ? : } - 15 ? '#92400e' : priceDiff > 0 ? '#d97706' : '#1a7a4a', fontWeight: 500 }}> - {priceDiff > 15 - ? `Ihr Preis liegt ${Math.round(priceDiff)}% über dem Marktmedian — starke USPs nötig zur Rechtfertigung.` - : priceDiff > 5 - ? `Leicht über Marktmedian (+${Math.round(priceDiff)}%) — gut durch Qualität begründbar.` - : priceDiff > -5 - ? 'Im Marktdurchschnitt — gute Ausgangsposition.' - : `${Math.round(Math.abs(priceDiff))}% unter Marktmedian — Preiserhöhung oder schnelle Vermietung möglich.`} - - - )} - - {/* Price bar vs market */} - {marketRent && ( - - - Marktbereich {property.location.city} - - CHF {Math.round(marketRent * 0.7)}–{Math.round(marketRent * 1.4)}/m² - - - - - - - )} - - - {/* ── Active demand ── */} - - Aktive Nachfrage - - Unternehmen, die aktuell in {property.location.city} suchen - - {matchingNeeds.length > 0 ? ( - <> - - {matchingNeeds.length} - aktive Suchprofile für diesen Typ & Standort - - {matchingNeeds.slice(0, 4).map(n => ( - - - {n.companyName} - - {n.requiredArea?.min ?? 0}–{n.requiredArea?.max ?? 0} m² - {n.budgetRange?.maxPerSqm ? ` · max. CHF ${n.budgetRange.maxPerSqm}/m²` : ''} - - - - - ))} - {matchingNeeds.length > 4 && ( - - + {matchingNeeds.length - 4} weitere Suchprofile - - )} - - ) : ( - - Keine aktiven Suchprofile für diesen Typ und Standort. - - )} - {intel && ( - - - Ø Vermietungsdauer vergleichbarer Objekte in {property.location.city}:{' '} - {intel.avgDaysOnMarket} Tage - - - )} - - - {/* ── Selling arguments ── */} - {sellingArgs.length > 0 && ( - - Verkaufsargumente für das Gespräch - - Stärken dieses Objekts — maßgeschneidert auf typische Mieterwünsche - - - {strongArgs.length > 0 && ( - - - Starke Argumente - - {strongArgs.map((arg, i) => ( - - - - {arg.title} - {arg.detail} - - - ))} - - )} - - {mediumArgs.length > 0 && ( - - - Weitere Vorteile - - {mediumArgs.map((arg, i) => ( - - - - {arg.title} - {arg.detail} - - - ))} - - )} - - )} - - {/* ── Proactive weakness handling ── */} - {weaknesses.length > 0 && ( - - Schwächen proaktiv adressieren - - Potenzielle Einwände kennen und entkräften — bevor der Interessent fragt - - {weaknesses.map((w, i) => ( - - - ⚠ {w.issue} - - - → {w.mitigation} - - {i < weaknesses.length - 1 && } - - ))} - - )} - - {/* ── Tenant fit ── */} - {intel && intel.dominantIndustryClusters.length > 0 && ( - - Welche Mieter passen? - - Dominant präsente Branchen in {property.location.city} — hohes Match-Potenzial - - - {intel.dominantIndustryClusters.map(c => ( - - ))} - - - - Nachfragestärke: {' '} - {{ LOW: 'Gering', MEDIUM: 'Mittel', HIGH: 'Hoch', VERY_HIGH: 'Sehr hoch' }[intel.demandStrength]} - - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyCard.tsx deleted file mode 100644 index cc7d20f..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyCard.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { Box, Button, Card, CardContent, Chip, LinearProgress, Typography } from '@mui/material' -import type { Property } from '../../domain/property' -import { FreshnessStatus } from '../../domain/enums' -import { - getAssetTypeColor, - getAssetTypeLabel, - getAvailabilityChipColor, - getAvailabilityLabel, - getResultTypeColor, - getResultTypeLabel, - qualityColor, -} from './propertyHelpers' - -export interface PropertyCardProps { - property: Property - matchScore?: number - positiveFactors?: string[] - topTradeoff?: string - selected?: boolean - onSelect?: () => void - onViewDetail?: () => void - onSaveToShortlist?: () => void - onFindMatches?: () => void -} - -const STALE_STATUSES: FreshnessStatus[] = [FreshnessStatus.STALE, FreshnessStatus.OUTDATED] - -export function PropertyCard({ - property: p, - matchScore, - positiveFactors, - topTradeoff, - selected, - onSelect, - onViewDetail, - onSaveToShortlist, - onFindMatches, -}: PropertyCardProps) { - const isStale = STALE_STATUSES.includes(p.dataQuality.freshness) - const isLowConfidence = p.confidenceScore < 0.65 - - const borderLeft = selected ? '4px solid #1e3a5f' : '4px solid transparent' - - return ( - - - {/* Header Zone */} - - - - - {isStale && } - {isLowConfidence && } - - - {/* Primary Zone */} - - {p.title} - - - {p.address.street} {p.address.houseNumber}, {p.address.city} - {p.location.canton ? ` · ${p.location.canton}` : ''} - - - - Fläche - {p.areaSqm.toLocaleString('de-CH')} m² - - - Miete/m²/Jahr - - {p.rentPricePerSqm > 0 ? `CHF ${p.rentPricePerSqm}` : k.A.} - - - - Verfügbar ab - - {p.availabilityDate ? new Date(p.availabilityDate).toLocaleDateString('de-CH') : '–'} - - - - - {/* Matchability Zone */} - {matchScore !== undefined ? ( - - - = 80 ? '#1a7a4a' : matchScore >= 60 ? '#d97706' : '#c0392b', color: 'white' }} - /> - - {positiveFactors && positiveFactors.length > 0 && ( - - {positiveFactors.slice(0, 3).map((f, i) => ( - ✓ {f} - ))} - - )} - {topTradeoff && ( - ⚠ {topTradeoff} - )} - - ) : ( - - - Bedarfsprofil wählen für Matchbarkeit - - - )} - - {/* Data Quality Zone */} - - - Datenqualität - {Math.round(p.dataQuality.score * 100)}% - - - {p.dataQuality.missingCriticalFields.length > 0 && ( - - {p.dataQuality.missingCriticalFields.slice(0, 3).map(f => ( - - ))} - {p.dataQuality.missingCriticalFields.length > 3 && ( - +{p.dataQuality.missingCriticalFields.length - 3} mehr - )} - - )} - - - {/* Action Zone */} - e.stopPropagation()}> - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyDetailSkeleton.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyDetailSkeleton.tsx deleted file mode 100644 index 641b28d..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyDetailSkeleton.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Box, Skeleton, Tab, Tabs } from '@mui/material' - -export function PropertyDetailSkeleton() { - return ( - - - - - - - - - - - - - - - - - {['Übersicht', 'Hard Facts', 'Soft Factors'].map(label => ( - - ))} - - - {Array.from({ length: 6 }).map((_, i) => ( - - - - - ))} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyDetailView.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyDetailView.tsx deleted file mode 100644 index 52e4ee5..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyDetailView.tsx +++ /dev/null @@ -1,397 +0,0 @@ -import { useState } from 'react' -import { - Alert, - Box, - Chip, - Divider, - IconButton, - LinearProgress, - Tab, - Tabs, - Typography, -} from '@mui/material' -import { X } from 'lucide-react' -import { NegotiationInsightsPanel } from './NegotiationInsightsPanel' -import { DataQualityPanel as DQPanel, ProvenancePanel } from '../data-quality' -import type { Property } from '../../domain/property' -import type { Match } from '../../domain/match' -import type { FutureSignal } from '../../domain/futureSignal' -import { usePropertyById, usePropertyMatches, usePropertySignals } from '../../hooks/useProperties' -import { PropertyDetailSkeleton } from './PropertyDetailSkeleton' -import { - getAssetTypeColor, - getAssetTypeLabel, - getAvailabilityChipColor, - getAvailabilityLabel, - getResultTypeColor, - getResultTypeLabel, - qualityColor, -} from './propertyHelpers' - -interface PropertyDetailViewProps { - propertyId: string - onClose?: () => void -} - -// ── Sub-components ──────────────────────────────────────────────────────────── - -function Field({ label, value }: { label: string; value?: string | number | boolean | null }) { - return ( - - - {label} - - {value !== undefined && value !== null && value !== '' ? ( - - {typeof value === 'boolean' ? (value ? 'Ja' : 'Nein') : String(value)} - - ) : ( - - )} - - ) -} - -function FieldGrid({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) -} - -function SectionTitle({ title }: { title: string }) { - return ( - - {title} - - ) -} - -function ScoreRow({ label, value }: { label: string; value?: number }) { - if (value === undefined || value === null) { - return ( - - - {label} - - - - - ) - } - const pct = value > 1 ? value : value * 100 - return ( - - - {label} - {Math.round(pct)} - - = 70 ? 'success' : pct >= 40 ? 'warning' : 'error'} - sx={{ height: 4, borderRadius: 2 }} - /> - - ) -} - -// ── Tab panels ──────────────────────────────────────────────────────────────── - -function OverviewPanel({ p }: { p: Property }) { - return ( - - - {[ - { label: 'Fläche', value: `${p.areaSqm.toLocaleString('de-CH')} m²` }, - { label: 'Miete/m²/Jahr', value: p.rentPricePerSqm > 0 ? `CHF ${p.rentPricePerSqm}` : undefined }, - { label: 'Verfügbar ab', value: p.availabilityDate ? new Date(p.availabilityDate).toLocaleDateString('de-CH') : undefined }, - ].map(({ label, value }) => ( - - {label} - - {value ?? k.A.} - - - ))} - - {p.description && ( - <> - - - {p.description} - - - )} - - - Score - {Math.round(p.dataQuality.score * 100)}% - - - {p.dataQuality.warnings.length > 0 && ( - - {p.dataQuality.warnings.map((w, i) => ( - ⚠ {w} - ))} - - )} - - ) -} - -function HardFactsPanel({ p }: { p: Property }) { - const hf = p.hardFacts - return ( - - - - - - - - - - - - - - - - - - - 0 ? `CHF ${p.rentPricePerSqm}` : undefined} /> - - - - - - - - - - - - - {hf && ( - <> - - - - - - - - - - - - - - - )} - - ) -} - -function SoftFactorsPanel({ p }: { p: Property }) { - const sf = p.softFactors - if (!sf) { - return Keine Soft Factors vorhanden. - } - const prestige = sf.prestigeScore ?? sf.prestige - const visibility = sf.visibilityScore - const footfall = sf.footfallScore - const commuter = sf.commuterAccessScore ?? sf.accessibility - const talent = sf.talentAccessScore ?? sf.talentAccess - const esg = sf.esgScore - const flex = sf.flexibilityScore - const expansion = sf.expansionPotentialScore - const tax = sf.taxEnvironmentScore - - return ( - - - - - - - - - - - - {sf.parkingSpots !== undefined && ( - - )} - {sf.publicTransportMinutes !== undefined && ( - - )} - {sf.infrastructureNotes && ( - - )} - - ) -} - -function MatchabilityPanel({ matches }: { matches: Match[] }) { - if (matches.length === 0) { - return ( - - Keine Matches für dieses Objekt vorhanden. - - ) - } - return ( - - - {matches.map(m => ( - - - Bedarf: {m.needId} - = 80 ? '#1a7a4a' : m.matchScore >= 60 ? '#d97706' : '#c0392b', - color: 'white', - }} - /> - - {m.positiveFactors?.slice(0, 3).map((f, i) => ( - ✓ {f.explanation ?? f.criterion} - ))} - {m.missingData && m.missingData.length > 0 && ( - - ⚠ {m.missingData.length} fehlende Datenfelder - - )} - - ))} - - ) -} - - -function SignalsPanel({ signals }: { signals: FutureSignal[] }) { - if (signals.length === 0) { - return Keine Zukunftssignale für dieses Objekt. - } - return ( - - - {signals.map(s => ( - - - {s.title ?? s.signalType} - - - {s.locationHint} · Konfidenz {Math.round(s.confidenceScore * 100)}% · {s.timeHorizonMonths} Monate - - {s.disclaimer && ( - - {s.disclaimer} - - )} - - ))} - - ) -} - -// ── Main component ──────────────────────────────────────────────────────────── - -const TABS = ['Übersicht', 'Verhandlung & Markt', 'Hard Facts', 'Soft Factors', 'Matchability', 'Datenqualität', 'Quelle', 'Signale'] as const - -export function PropertyDetailView({ propertyId, onClose }: PropertyDetailViewProps) { - const [tab, setTab] = useState(0) - const { data: property, isLoading } = usePropertyById(propertyId) - const { data: matches = [] } = usePropertyMatches(propertyId) - const { data: signals = [] } = usePropertySignals(propertyId) - - if (isLoading) return - if (!property) { - return ( - - Objekt nicht gefunden. - - ) - } - - const isLowQuality = property.dataQuality.score < 0.6 - const hasCriticalGaps = property.dataQuality.missingCriticalFields.length > 0 - - return ( - - {/* Header */} - - - - - - - - {onClose && ( - - - - )} - - - {property.title} - - - {property.address.street} {property.address.houseNumber}, {property.address.postalCode} {property.address.city} - - - {isLowQuality && ( - - Niedrige Datenqualität ({Math.round(property.dataQuality.score * 100)}%) — Angaben können unvollständig sein. - - )} - {hasCriticalGaps && !isLowQuality && ( - - Kritische Felder fehlen: {property.dataQuality.missingCriticalFields.slice(0, 3).join(', ')} - - )} - - - {/* Tabs */} - setTab(v)} - variant="scrollable" - scrollButtons="auto" - sx={{ - borderBottom: '1px solid #e2e8f0', - flexShrink: 0, - '& .MuiTab-root': { textTransform: 'none', fontSize: '0.8rem', minWidth: 'auto', px: 1.5 }, - }} - > - {TABS.map(label => ( - - ))} - - - {/* Tab content */} - - {tab === 0 && } - {tab === 1 && } - {tab === 2 && } - {tab === 3 && } - {tab === 4 && } - {tab === 5 && } - {tab === 6 && } - {tab === 7 && } - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyFilterBar.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyFilterBar.tsx deleted file mode 100644 index 2d68893..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyFilterBar.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { - Box, - Button, - Card, - Chip, - MenuItem, - Select, - TextField, - Typography, -} from '@mui/material' -import { AssetType, AvailabilityStatus } from '../../domain/enums' -import { getAssetTypeLabel, getAvailabilityLabel } from './propertyHelpers' - -export interface PropertyTableFilters { - search?: string - assetTypes?: string[] - availabilityStatus?: string - sortBy?: 'dataQuality' | 'availability' | 'area' | 'rent' | 'confidence' - sortDir?: 'asc' | 'desc' -} - -interface PropertyFilterBarProps { - filters: PropertyTableFilters - onFiltersChange: (f: PropertyTableFilters) => void -} - -const SORT_OPTIONS = [ - { value: 'dataQuality', label: 'Datenqualität' }, - { value: 'area', label: 'Fläche' }, - { value: 'rent', label: 'Miete' }, - { value: 'confidence', label: 'Konfidenz' }, - { value: 'availability', label: 'Verfügbarkeit' }, -] as const - -const ALL_ASSET_TYPES = Object.values(AssetType) -const ALL_AVAILABILITY = Object.values(AvailabilityStatus) - -export function PropertyFilterBar({ filters, onFiltersChange }: PropertyFilterBarProps) { - const isActive = - !!filters.search || - (filters.assetTypes?.length ?? 0) > 0 || - !!filters.availabilityStatus || - !!filters.sortBy - - function update(partial: Partial) { - onFiltersChange({ ...filters, ...partial }) - } - - function reset() { - onFiltersChange({}) - } - - const selectedAssetTypes = filters.assetTypes ?? [] - - function toggleAssetType(type: string) { - const current = selectedAssetTypes - if (current.includes(type)) { - update({ assetTypes: current.filter(t => t !== type) }) - } else { - update({ assetTypes: [...current, type] }) - } - } - - return ( - - - - {/* Row 1: Asset type chips */} - - - Typ: - - {ALL_ASSET_TYPES.map(t => ( - toggleAssetType(t)} - sx={{ - cursor: 'pointer', - ...(selectedAssetTypes.includes(t) && { bgcolor: '#1e3a5f', color: 'white', borderColor: '#1e3a5f' }), - }} - /> - ))} - - - {/* Row 2: Availability, Sort, Search, Reset */} - - - - - - update({ search: e.target.value || undefined })} - placeholder="Titel, Stadt, Strasse …" - size="small" - sx={{ flex: 1, minWidth: 220 }} - /> - - {isActive && ( - - )} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyTable.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyTable.tsx deleted file mode 100644 index d7c2563..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/PropertyTable.tsx +++ /dev/null @@ -1,266 +0,0 @@ -import { - Alert, - Box, - Chip, - IconButton, - LinearProgress, - Skeleton, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Tooltip, - Typography, -} from '@mui/material' -import { Bookmark, Eye } from 'lucide-react' -import type { Property } from '../../domain/property' -import type { PropertyTableFilters } from './PropertyFilterBar' -import { - getAssetTypeColor, - getAssetTypeLabel, - getAvailabilityChipColor, - getAvailabilityLabel, - getResultTypeColor, - getResultTypeLabel, - qualityColor, -} from './propertyHelpers' - -interface PropertyTableProps { - properties: Property[] - isLoading: boolean - isError: boolean - selectedId: string | null - onSelect: (id: string) => void - onViewDetail?: (id: string) => void - filters: PropertyTableFilters - onFiltersChange: (f: PropertyTableFilters) => void -} - -const COL_HEADERS = [ - 'Objekt', 'Typ', 'Standort', 'Fläche', 'Miete/m²', - 'Verfügbarkeit', 'Konfidenz', 'Datenqualität', 'Quelle', 'Aktionen', -] - -function LoadingRows() { - return ( - <> - {Array.from({ length: 5 }).map((_, i) => ( - - {COL_HEADERS.map((h) => ( - - - - ))} - - ))} - - ) -} - -export function PropertyTable({ - properties, - isLoading, - isError, - selectedId, - onSelect, - onViewDetail, - filters, - onFiltersChange, -}: PropertyTableProps) { - if (isError) { - return Objekte konnten nicht geladen werden. - } - - function handleSort(field: PropertyTableFilters['sortBy']) { - if (filters.sortBy === field) { - onFiltersChange({ ...filters, sortDir: filters.sortDir === 'asc' ? 'desc' : 'asc' }) - } else { - onFiltersChange({ ...filters, sortBy: field, sortDir: 'desc' }) - } - } - - function SortableHeader({ field, label }: { field: PropertyTableFilters['sortBy']; label: string }) { - const isActive = filters.sortBy === field - return ( - handleSort(field)} - > - {label}{isActive ? (filters.sortDir === 'asc' ? ' ↑' : ' ↓') : ''} - - ) - } - - return ( - - - - - Objekt - Typ - Standort - - - - - - Quelle - Aktionen - - - - {isLoading ? ( - - ) : properties.length === 0 ? ( - - - - Keine Objekte gefunden. - - - - ) : ( - properties.map(p => { - const isSelected = p.id === selectedId - const qScore = p.dataQuality.score - const hasCritical = p.dataQuality.missingCriticalFields.length > 0 - - return ( - onSelect(p.id)} - sx={{ - cursor: 'pointer', - bgcolor: isSelected - ? 'rgba(30,58,95,0.06)' - : hasCritical - ? 'rgba(192,57,43,0.03)' - : 'inherit', - borderLeft: isSelected ? '3px solid #1e3a5f' : '3px solid transparent', - '&:hover': { bgcolor: isSelected ? 'rgba(30,58,95,0.08)' : 'rgba(0,0,0,0.02)' }, - }} - > - {/* Objekt */} - - - {p.title} - - - {p.address.street} {p.address.houseNumber}, {p.address.city} - - - - {/* Typ */} - - - - - {/* Standort */} - - {p.location.city} - {p.location.canton && ( - {p.location.canton} - )} - - - {/* Fläche */} - - {p.areaSqm.toLocaleString('de-CH')} - - - {/* Miete */} - - - {p.rentPricePerSqm > 0 ? `CHF ${p.rentPricePerSqm}` : k.A.} - - - - {/* Verfügbarkeit */} - - - - - {/* Konfidenz */} - - = 0.85 ? '#1a7a4a' : p.confidenceScore >= 0.65 ? '#1e3a5f' : '#d97706', - }} - > - {Math.round(p.confidenceScore * 100)}% - - - - {/* Datenqualität */} - - - - - - {Math.round(qScore * 100)}% - - - - - - {/* Quelle */} - - - - - {/* Aktionen */} - e.stopPropagation()}> - - - onViewDetail ? onViewDetail(p.id) : onSelect(p.id)}> - - - - - - - - - - - - ) - }) - )} - -
-
- ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/QuickActionPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/QuickActionPanel.tsx deleted file mode 100644 index 944cc85..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/QuickActionPanel.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Box, Button, Card, CardContent, Typography } from '@mui/material' -import { useNavigate } from 'react-router' - -const ACTIONS = [ - { label: 'Objekte verwalten', path: '/supply/properties' }, - { label: 'Match Center', path: '/supply/match-center' }, - { label: 'Marktchancen', path: '/supply/future-availability' }, - { label: 'Datenqualität', path: '/supply/data-quality' }, - { label: 'Review Queue', path: '/ops/review-queue' }, - { label: 'Market Intelligence', path: '/ops/market-intelligence' }, -] as const - -export function QuickActionPanel() { - const navigate = useNavigate() - - return ( - - - - Schnellzugriff - - - {ACTIONS.map(action => ( - - ))} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/ReviewTaskWidget.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/ReviewTaskWidget.tsx deleted file mode 100644 index 7a66e19..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/ReviewTaskWidget.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Box, Button, Card, CardContent, Chip, Typography } from '@mui/material' -import type { DashboardReviewTask } from '../../domain/dashboard' - -interface ReviewTaskWidgetProps { - tasks: DashboardReviewTask[] - onNavigate: () => void -} - -const PRIORITY_LABELS: Record = { - HIGH: 'Hoch', - MEDIUM: 'Mittel', - LOW: 'Niedrig', -} - -const STATUS_LABELS: Record = { - PENDING: 'Ausstehend', - IN_REVIEW: 'In Bearbeitung', - COMPLETED: 'Abgeschlossen', -} - -function priorityColor(priority: string): 'error' | 'warning' | 'default' { - if (priority === 'HIGH') return 'error' - if (priority === 'MEDIUM') return 'warning' - return 'default' -} - -function statusColor(status: string): 'warning' | 'info' | 'success' | 'default' { - if (status === 'PENDING') return 'warning' - if (status === 'IN_REVIEW') return 'info' - if (status === 'COMPLETED') return 'success' - return 'default' -} - -const PRIORITY_ORDER: Record = { HIGH: 0, MEDIUM: 1, LOW: 2 } - -export function ReviewTaskWidget({ tasks, onNavigate }: ReviewTaskWidgetProps) { - const sorted = [...tasks].sort( - (a, b) => (PRIORITY_ORDER[a.priority] ?? 9) - (PRIORITY_ORDER[b.priority] ?? 9), - ) - - return ( - - - - - Review Queue - - - - - {sorted.length === 0 ? ( - - Keine offenen Aufgaben. - - ) : ( - - {sorted.map(task => ( - - - - {task.title} - - - - ))} - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/StrongMatchMiniCard.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/StrongMatchMiniCard.tsx deleted file mode 100644 index c02bf23..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/StrongMatchMiniCard.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Box, Button, Card, CardContent, Chip, Typography } from '@mui/material' -import { useNavigate } from 'react-router' -import type { StrongMatchItem } from '../../domain/dashboard' - -interface StrongMatchMiniCardProps { - match: StrongMatchItem -} - -function scoreColor(score: number): string { - if (score >= 80) return '#1a7a4a' - if (score >= 60) return '#d97706' - return '#c0392b' -} - -export function StrongMatchMiniCard({ match }: StrongMatchMiniCardProps) { - const navigate = useNavigate() - - return ( - - - - - {match.propertyTitle} - - - - - - {match.propertyAddress} - - - {match.topReason} - - - - {match.missingDataCount > 0 && ( - - )} - - - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/StrongMatchOverview.tsx b/.claude/worktrees/agent-a82a3716/src/components/supply/StrongMatchOverview.tsx deleted file mode 100644 index af529ff..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/StrongMatchOverview.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Box, Button, Card, CardContent, Typography } from '@mui/material' -import type { StrongMatchItem } from '../../domain/dashboard' -import { StrongMatchMiniCard } from './StrongMatchMiniCard' - -interface StrongMatchOverviewProps { - matches: StrongMatchItem[] - onNavigate: () => void -} - -export function StrongMatchOverview({ matches, onNavigate }: StrongMatchOverviewProps) { - return ( - - - - - Starke Matches - - - - - {matches.length === 0 ? ( - - Keine starken Matches vorhanden. - - ) : ( - matches.slice(0, 5).map(m => ( - - )) - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/index.ts b/.claude/worktrees/agent-a82a3716/src/components/supply/index.ts deleted file mode 100644 index 1d61c2b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -export { KpiCard } from './KpiCard' -export { KpiGrid } from './KpiGrid' -export { StrongMatchMiniCard } from './StrongMatchMiniCard' -export { StrongMatchOverview } from './StrongMatchOverview' -export { DataQualityWidget } from './DataQualityWidget' -export { FutureSignalWidget } from './FutureSignalWidget' -export { ReviewTaskWidget } from './ReviewTaskWidget' -export { QuickActionPanel } from './QuickActionPanel' -export { DashboardSkeleton } from './DashboardSkeleton' -export { DashboardHeader } from './DashboardHeader' -export { PropertyCard } from './PropertyCard' -export type { PropertyCardProps } from './PropertyCard' -export { PropertyTable } from './PropertyTable' -export { PropertyFilterBar } from './PropertyFilterBar' -export type { PropertyTableFilters } from './PropertyFilterBar' -export { PropertyDetailView } from './PropertyDetailView' -export { PropertyDetailSkeleton } from './PropertyDetailSkeleton' diff --git a/.claude/worktrees/agent-a82a3716/src/components/supply/propertyHelpers.ts b/.claude/worktrees/agent-a82a3716/src/components/supply/propertyHelpers.ts deleted file mode 100644 index db405e2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/supply/propertyHelpers.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { AssetType, AvailabilityStatus, ResultType } from '../../domain/enums' - -export function getAssetTypeLabel(type: AssetType): string { - const labels: Record = { - OFFICE: 'Büro', - LOGISTICS: 'Logistik', - RETAIL: 'Retail', - GASTRO: 'Gastro', - PRODUCTION: 'Produktion', - MIXED: 'Gemischt', - LIGHT_INDUSTRIAL: 'Leichtindustrie', - UNKNOWN: 'Unbekannt', - } - return labels[type] ?? type -} - -export function getAssetTypeColor(type: AssetType): string { - const colors: Record = { - OFFICE: '#1e3a5f', - LOGISTICS: '#d97706', - RETAIL: '#7c3aed', - GASTRO: '#0d9488', - PRODUCTION: '#92400e', - MIXED: '#6b7280', - LIGHT_INDUSTRIAL: '#b45309', - UNKNOWN: '#9ca3af', - } - return colors[type] ?? '#6b7280' -} - -export function getAvailabilityLabel(status: AvailabilityStatus): string { - const labels: Record = { - AVAILABLE_NOW: 'Verfügbar', - AVAILABLE_SOON: 'Bald verfügbar', - FUTURE_SIGNAL: 'Zukunftssignal', - OCCUPIED: 'Belegt', - UNKNOWN: 'Unbekannt', - } - return labels[status] ?? status -} - -export function getAvailabilityChipColor(status: AvailabilityStatus): 'success' | 'warning' | 'secondary' | 'error' | 'default' { - const colors: Record = { - AVAILABLE_NOW: 'success', - AVAILABLE_SOON: 'warning', - FUTURE_SIGNAL: 'secondary', - OCCUPIED: 'error', - UNKNOWN: 'default', - } - return colors[status] ?? 'default' -} - -export function getResultTypeLabel(type: ResultType): string { - const labels: Record = { - VERIFIED_PORTFOLIO: 'Portfolio', - EXTERNAL_MARKET: 'Markt', - FUTURE_AVAILABILITY: 'Zukunft', - } - return labels[type] ?? type -} - -export function getResultTypeColor(type: ResultType): string { - const colors: Record = { - VERIFIED_PORTFOLIO: '#1e3a5f', - EXTERNAL_MARKET: '#1a7a4a', - FUTURE_AVAILABILITY: '#7c3aed', - } - return colors[type] ?? '#6b7280' -} - -export function qualityColor(score: number): 'success' | 'warning' | 'error' { - if (score >= 0.8) return 'success' - if (score >= 0.6) return 'warning' - return 'error' -} - -export function confidenceColor(score: number): string { - if (score >= 0.85) return '#1a7a4a' - if (score >= 0.65) return '#1e3a5f' - return '#d97706' -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/tables/DataTableShell.tsx b/.claude/worktrees/agent-a82a3716/src/components/tables/DataTableShell.tsx deleted file mode 100644 index fbb1b0a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/tables/DataTableShell.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Paper, Table, TableBody, TableContainer } from '@mui/material' -import type { SxProps, Theme } from '@mui/material' -import type { ReactNode } from 'react' -import { LoadingRows } from './LoadingRows' -import { EmptyTableState } from './EmptyTableState' - -interface DataTableShellProps { - toolbar?: ReactNode - children: ReactNode - loading?: boolean - loadingRows?: number - loadingCols?: number - empty?: boolean - emptySlot?: ReactNode - emptyColSpan?: number - stickyHeader?: boolean - size?: 'small' | 'medium' - sx?: SxProps -} - -export function DataTableShell({ - toolbar, - children, - loading = false, - loadingRows = 5, - loadingCols = 6, - empty = false, - emptySlot, - emptyColSpan = 6, - stickyHeader = false, - size = 'small', - sx, -}: DataTableShellProps) { - return ( - - {toolbar} - - - {!loading && children} - {loading && ( - - - - )} - {!loading && empty && ( - - {emptySlot ?? } - - )} -
-
-
- ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/tables/EmptyTableState.tsx b/.claude/worktrees/agent-a82a3716/src/components/tables/EmptyTableState.tsx deleted file mode 100644 index 908bdbd..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/tables/EmptyTableState.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Box, TableCell, TableRow, Typography } from '@mui/material' -import { Inbox } from 'lucide-react' - -interface EmptyTableStateProps { - colSpan: number - title?: string - description?: string -} - -export function EmptyTableState({ colSpan, title, description }: EmptyTableStateProps) { - return ( - - - - - - {title ?? 'Keine Einträge gefunden'} - - {description && ( - - {description} - - )} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/tables/FilterBar.tsx b/.claude/worktrees/agent-a82a3716/src/components/tables/FilterBar.tsx deleted file mode 100644 index f5328bc..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/tables/FilterBar.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Box, Chip, Typography } from '@mui/material' -import type { SxProps, Theme } from '@mui/material' - -interface ActiveFilter { - key: string - label: string - onRemove: () => void -} - -interface FilterBarProps { - filters: ActiveFilter[] - onClearAll?: () => void - sx?: SxProps -} - -export function FilterBar({ filters, onClearAll, sx }: FilterBarProps) { - if (filters.length === 0) return null - - return ( - - {filters.map(f => ( - - ))} - {onClearAll && filters.length > 1 && ( - - Alle löschen - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/tables/LoadingRows.tsx b/.claude/worktrees/agent-a82a3716/src/components/tables/LoadingRows.tsx deleted file mode 100644 index e51af68..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/tables/LoadingRows.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Skeleton, TableCell, TableRow } from '@mui/material' - -interface LoadingRowsProps { - rows?: number - cols?: number -} - -export function LoadingRows({ rows = 5, cols = 6 }: LoadingRowsProps) { - return ( - <> - {Array.from({ length: rows }).map((_, ri) => ( - - {Array.from({ length: cols }).map((_, ci) => ( - - - - ))} - - ))} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/tables/TableToolbar.tsx b/.claude/worktrees/agent-a82a3716/src/components/tables/TableToolbar.tsx deleted file mode 100644 index c7e2e7b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/tables/TableToolbar.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Box, Chip, Typography } from '@mui/material' -import type { SxProps, Theme } from '@mui/material' -import type { ReactNode } from 'react' - -interface TableToolbarProps { - title?: string - count?: number - filterSlot?: ReactNode - actions?: ReactNode - sx?: SxProps -} - -export function TableToolbar({ title, count, filterSlot, actions, sx }: TableToolbarProps) { - return ( - - - {title && ( - - {title} - - )} - {count !== undefined && ( - - )} - {filterSlot} - - {actions && ( - - {actions} - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/tables/index.ts b/.claude/worktrees/agent-a82a3716/src/components/tables/index.ts deleted file mode 100644 index b489583..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/tables/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { DataTableShell } from './DataTableShell' -export { TableToolbar } from './TableToolbar' -export { FilterBar } from './FilterBar' -export { EmptyTableState } from './EmptyTableState' -export { LoadingRows } from './LoadingRows' diff --git a/.claude/worktrees/agent-a82a3716/src/components/ui/AppErrorBoundary.tsx b/.claude/worktrees/agent-a82a3716/src/components/ui/AppErrorBoundary.tsx deleted file mode 100644 index 3f67fdd..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/AppErrorBoundary.tsx +++ /dev/null @@ -1,45 +0,0 @@ -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/.claude/worktrees/agent-a82a3716/src/components/ui/CardSkeleton.tsx b/.claude/worktrees/agent-a82a3716/src/components/ui/CardSkeleton.tsx deleted file mode 100644 index 6cfc3e2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/CardSkeleton.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Box, Card, CardContent, Skeleton } from '@mui/material' - -interface CardSkeletonProps { - lines?: number - hasHeader?: boolean - hasActions?: boolean -} - -export function CardSkeleton({ lines = 3, hasHeader = true, hasActions = false }: CardSkeletonProps) { - return ( - - - {hasHeader && ( - - - - - )} - - {Array.from({ length: lines }).map((_, i) => ( - - ))} - - {hasActions && ( - - - - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ui/ConfirmDialog.tsx b/.claude/worktrees/agent-a82a3716/src/components/ui/ConfirmDialog.tsx deleted file mode 100644 index 08c8bee..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/ConfirmDialog.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material' -import { AlertTriangle } from 'lucide-react' - -interface Props { - open: boolean - title: string - message: string - confirmLabel?: string - cancelLabel?: string - destructive?: boolean - onConfirm: () => void - onCancel: () => void -} - -export function ConfirmDialog({ - open, - title, - message, - confirmLabel = 'Bestätigen', - cancelLabel = 'Abbrechen', - destructive = false, - onConfirm, - onCancel, -}: Props) { - return ( - - - {destructive && } - {title} - - - - {message} - - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ui/DecisionContextPanel.tsx b/.claude/worktrees/agent-a82a3716/src/components/ui/DecisionContextPanel.tsx deleted file mode 100644 index aa37e53..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/DecisionContextPanel.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { useState } from 'react' -import { Alert, Box, Button, Chip, Collapse, IconButton, Typography } from '@mui/material' -import { AlertTriangle, ChevronDown, ChevronUp, Target } from 'lucide-react' -import type { ReactNode } from 'react' - -export interface DecisionMetric { - label: string - value: string | number - severity?: 'neutral' | 'positive' | 'warning' | 'critical' -} - -export interface DecisionAction { - label: string - primary?: boolean - onClick: () => void -} - -interface Props { - /** The core question this screen answers */ - decision: string - /** One-line explanation of why this decision matters */ - context?: string - /** Key data points relevant to the decision */ - metrics?: DecisionMetric[] - /** Missing data that could affect the decision */ - missing?: string[] - /** Active risks the user should be aware of */ - risks?: string[] - /** Available actions — first primary action is highlighted */ - actions?: DecisionAction[] - /** Custom content after the standard rows */ - children?: ReactNode -} - -const SEVERITY_COLOR: Record, string> = { - neutral: '#f1f5f9', - positive: '#f0fdf4', - warning: '#fef9c3', - critical: '#fef2f2', -} - -const SEVERITY_TEXT: Record, string> = { - neutral: '#475569', - positive: '#1a7a4a', - warning: '#92400e', - critical: '#991b1b', -} - -export function DecisionContextPanel({ - decision, - context, - metrics = [], - missing = [], - risks = [], - actions = [], - children, -}: Props) { - const [expanded, setExpanded] = useState(false) - const hasDetails = missing.length > 0 || risks.length > 0 || !!children - - return ( - - {/* Main row */} - - {/* Decision question */} - - - - - {decision} - - {context && ( - - {context} - - )} - - - - {/* Metric chips */} - {metrics.length > 0 && ( - - {metrics.map((m, i) => { - const sev = m.severity ?? 'neutral' - return ( - - ) - })} - - )} - - {/* Actions + expand toggle */} - - {actions.map((a, i) => ( - - ))} - {hasDetails && ( - setExpanded(v => !v)} - sx={{ color: '#64748b', width: 24, height: 24 }} - > - {expanded ? : } - - )} - - - - {/* Expandable details */} - - - {risks.length > 0 && ( - } sx={{ py: 0.25, '& .MuiAlert-message': { fontSize: '0.75rem' } }}> - Risiken:{' '}{risks.join(' · ')} - - )} - {missing.length > 0 && ( - - Fehlende Daten:{' '}{missing.join(' · ')} - - )} - {children} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ui/EmptyState.tsx b/.claude/worktrees/agent-a82a3716/src/components/ui/EmptyState.tsx deleted file mode 100644 index 3a602fd..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/EmptyState.tsx +++ /dev/null @@ -1,29 +0,0 @@ -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/.claude/worktrees/agent-a82a3716/src/components/ui/ErrorState.tsx b/.claude/worktrees/agent-a82a3716/src/components/ui/ErrorState.tsx deleted file mode 100644 index 2b0b629..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/ErrorState.tsx +++ /dev/null @@ -1,19 +0,0 @@ -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/.claude/worktrees/agent-a82a3716/src/components/ui/LoadingPage.tsx b/.claude/worktrees/agent-a82a3716/src/components/ui/LoadingPage.tsx deleted file mode 100644 index c42ef6d..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/LoadingPage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -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/.claude/worktrees/agent-a82a3716/src/components/ui/PageContainer.tsx b/.claude/worktrees/agent-a82a3716/src/components/ui/PageContainer.tsx deleted file mode 100644 index cd77c2e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/PageContainer.tsx +++ /dev/null @@ -1,19 +0,0 @@ -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/.claude/worktrees/agent-a82a3716/src/components/ui/PanelLoadingState.tsx b/.claude/worktrees/agent-a82a3716/src/components/ui/PanelLoadingState.tsx deleted file mode 100644 index e7ee3fa..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/PanelLoadingState.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Box, Skeleton } from '@mui/material' - -interface PanelLoadingStateProps { - rows?: number - height?: number -} - -export function PanelLoadingState({ rows = 4, height = 16 }: PanelLoadingStateProps) { - return ( - - {Array.from({ length: rows }).map((_, i) => ( - - ))} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ui/RestrictedState.tsx b/.claude/worktrees/agent-a82a3716/src/components/ui/RestrictedState.tsx deleted file mode 100644 index fc3782f..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/RestrictedState.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Box, Typography } from '@mui/material' -import { ShieldOff } from 'lucide-react' -import type { SxProps, Theme } from '@mui/material' - -interface RestrictedStateProps { - message?: string - reason?: string - sx?: SxProps -} - -export function RestrictedState({ message, reason, sx }: RestrictedStateProps) { - return ( - - - - {message ?? 'Zugriff eingeschränkt'} - - {reason && ( - - {reason} - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ui/SectionContainer.tsx b/.claude/worktrees/agent-a82a3716/src/components/ui/SectionContainer.tsx deleted file mode 100644 index a2c9d25..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/SectionContainer.tsx +++ /dev/null @@ -1,29 +0,0 @@ -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/.claude/worktrees/agent-a82a3716/src/components/ui/ToastProvider.tsx b/.claude/worktrees/agent-a82a3716/src/components/ui/ToastProvider.tsx deleted file mode 100644 index 6b9d463..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/ToastProvider.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Alert, Snackbar, Stack } from '@mui/material' -import { useToastStore } from '../../stores/toastStore' - -export function ToastProvider() { - const { toasts, dismissToast } = useToastStore() - - return ( - - {toasts.map((toast) => ( - dismissToast(toast.id)} - anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} - sx={{ position: 'relative', transform: 'none', left: 'auto', bottom: 'auto' }} - > - dismissToast(toast.id)} - severity={toast.severity} - variant="filled" - sx={{ minWidth: 320, boxShadow: 3 }} - > - {toast.message} - - - ))} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ui/UnauthorizedState.tsx b/.claude/worktrees/agent-a82a3716/src/components/ui/UnauthorizedState.tsx deleted file mode 100644 index d3e4624..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/UnauthorizedState.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Box, Button, Typography } from '@mui/material' -import { Lock } from 'lucide-react' - -interface UnauthorizedStateProps { - message?: string - onLogin?: () => void -} - -export function UnauthorizedState({ message, onLogin }: UnauthorizedStateProps) { - return ( - - - - Nicht berechtigt - - {message && ( - - {message} - - )} - {onLogin && ( - - )} - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/components/ui/index.ts b/.claude/worktrees/agent-a82a3716/src/components/ui/index.ts deleted file mode 100644 index 9173e00..0000000 --- a/.claude/worktrees/agent-a82a3716/src/components/ui/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export { AppErrorBoundary } from './AppErrorBoundary' -export { LoadingPage } from './LoadingPage' -export { EmptyState } from './EmptyState' -export { ErrorState } from './ErrorState' -export { PageContainer } from './PageContainer' -export { SectionContainer } from './SectionContainer' -export { CardSkeleton } from './CardSkeleton' -export { PanelLoadingState } from './PanelLoadingState' -export { UnauthorizedState } from './UnauthorizedState' -export { RestrictedState } from './RestrictedState' -export { ToastProvider } from './ToastProvider' -export { ConfirmDialog } from './ConfirmDialog' -export { DecisionContextPanel } from './DecisionContextPanel' -export type { DecisionMetric, DecisionAction } from './DecisionContextPanel' diff --git a/.claude/worktrees/agent-a82a3716/src/domain/activityEvent.ts b/.claude/worktrees/agent-a82a3716/src/domain/activityEvent.ts deleted file mode 100644 index 3081916..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/activityEvent.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const ActivityAction = { - CREATED: 'CREATED', - UPDATED: 'UPDATED', - DELETED: 'DELETED', - APPROVED: 'APPROVED', - REJECTED: 'REJECTED', - SHORTLISTED: 'SHORTLISTED', - REVIEWED: 'REVIEWED', - VERIFIED: 'VERIFIED', - EXPORTED: 'EXPORTED', - SHARED: 'SHARED', -} as const -export type ActivityAction = typeof ActivityAction[keyof typeof ActivityAction] - -export const ActivityEntityType = { - PROPERTY: 'PROPERTY', - NEED: 'NEED', - MATCH: 'MATCH', - FUTURE_SIGNAL: 'FUTURE_SIGNAL', - AI_OUTPUT: 'AI_OUTPUT', - SHORTLIST: 'SHORTLIST', - USER: 'USER', -} as const -export type ActivityEntityType = typeof ActivityEntityType[keyof typeof ActivityEntityType] - -export interface ActivityEvent { - id: string - actorId: string // user or system ID that triggered the event - action: ActivityAction - entityType: ActivityEntityType - entityId: string - timestamp: string - metadata?: Record -} diff --git a/.claude/worktrees/agent-a82a3716/src/domain/aiOutput.ts b/.claude/worktrees/agent-a82a3716/src/domain/aiOutput.ts deleted file mode 100644 index cb84101..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/aiOutput.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { ReviewStatus } from './enums' - -export const AIOutputType = { - NEED_PARSE: 'NEED_PARSE', - FOLLOW_UP_QUESTIONS: 'FOLLOW_UP_QUESTIONS', - MATCH_EXPLANATION: 'MATCH_EXPLANATION', - COMPARE_SUMMARY: 'COMPARE_SUMMARY', - DECISION_BRIEF: 'DECISION_BRIEF', - DATA_QUALITY_SUMMARY: 'DATA_QUALITY_SUMMARY', -} as const -export type AIOutputType = typeof AIOutputType[keyof typeof AIOutputType] - -export const AIErrorType = { - SCHEMA_VALIDATION: 'SCHEMA_VALIDATION', - PROVIDER_TIMEOUT: 'PROVIDER_TIMEOUT', - INVALID_JSON: 'INVALID_JSON', - EMPTY_RESPONSE: 'EMPTY_RESPONSE', - RATE_LIMIT: 'RATE_LIMIT', -} as const -export type AIErrorType = typeof AIErrorType[keyof typeof AIErrorType] - -export interface AIOutputError { - type: AIErrorType - message: string - recoverable: boolean -} - -export interface AIOutput { - id: string - type: AIOutputType - provider: string - model: string - promptVersion: string - schemaVersion: string - inputHash: string - outputPreview: string - createdAt: string - latencyMs?: number - costEstimate?: number - reviewStatus: ReviewStatus - relatedEntityType: string - relatedEntityId: string - error?: AIOutputError -} diff --git a/.claude/worktrees/agent-a82a3716/src/domain/assistant.ts b/.claude/worktrees/agent-a82a3716/src/domain/assistant.ts deleted file mode 100644 index e12c5e6..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/assistant.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { WorkspaceType } from './enums' - -export interface AssistantContext { - currentRoute: string - workspace: WorkspaceType | null - selectedEntityType?: string - selectedEntityId?: string - visibleScores?: Record - visibleRisks?: string[] - visibleMissingData?: string[] - availableActions?: string[] - userRole: string - organizationId: string -} - -export interface AssistantAction { - id: string - label: string - description: string - actionType: 'NAVIGATE' | 'OPEN_REVIEW' | 'ADD_TO_SHORTLIST' | 'REQUEST_DATA' | 'SEND_TO_REVIEW' - payload?: Record -} - -export interface AssistantMessage { - id: string - role: 'user' | 'assistant' - content: string - createdAt: string - confidence?: number - sources?: string[] - actions?: AssistantAction[] -} - -export interface SuggestedQuestion { - id: string - question: string - category: string -} diff --git a/.claude/worktrees/agent-a82a3716/src/domain/dashboard.ts b/.claude/worktrees/agent-a82a3716/src/domain/dashboard.ts deleted file mode 100644 index be478b7..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/dashboard.ts +++ /dev/null @@ -1,64 +0,0 @@ -export interface KpiCardData { - id: string - label: string - value: number | string - trend?: 'up' | 'down' | 'neutral' - trendLabel?: string - accent?: string - tooltip?: string - onClick?: () => void -} - -export interface StrongMatchItem { - matchId: string - propertyId: string - propertyTitle: string - propertyAddress: string - needSummary: string - matchScore: number - topReason: string - missingDataCount: number - nextBestAction: string -} - -export interface TimeHorizonDistribution { - short: number // 0–6 months - medium: number // 6–12 months - long: number // 12–24 months -} - -export interface FutureSignalSummary { - total: number - highConfidence: number - restricted: number - needsReview: number - avgTimeHorizonMonths: number - timeHorizonDistribution: TimeHorizonDistribution -} - -export interface DataQualitySummary { - avgScore: number - critical: number - propertiesWithMissingCritical: number - topMissingFields: string[] -} - -export interface DashboardReviewTask { - id: string - title: string - priority: 'HIGH' | 'MEDIUM' | 'LOW' - status: string - type: string -} - -export interface DashboardData { - totalProperties: number - activeProperties: number - strongMatchCount: number - avgDataQuality: number - futureSignals: FutureSignalSummary | null - dataQuality: DataQualitySummary | null - reviewTasks: DashboardReviewTask[] | null - strongMatches: StrongMatchItem[] | null - lastUpdated: string -} diff --git a/.claude/worktrees/agent-a82a3716/src/domain/dataSource.ts b/.claude/worktrees/agent-a82a3716/src/domain/dataSource.ts deleted file mode 100644 index a0fbd41..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/dataSource.ts +++ /dev/null @@ -1,147 +0,0 @@ -import type { FreshnessStatus } from './enums' - -// ── Connector / Source Type ─────────────────────────────────────────────────── -export const DataSourceType = { - API_CONNECTOR: 'API_CONNECTOR', - CSV_IMPORT: 'CSV_IMPORT', - MANUAL_UPLOAD: 'MANUAL_UPLOAD', - PUBLIC_WEB_SOURCE: 'PUBLIC_WEB_SOURCE', - PARTNER_FEED: 'PARTNER_FEED', - INTERNAL_PORTFOLIO_EXPORT: 'INTERNAL_PORTFOLIO_EXPORT', - CONTRACT_METADATA_IMPORT: 'CONTRACT_METADATA_IMPORT', - ANALYST_ENTRY: 'ANALYST_ENTRY', - FUTURE_CRAWLER_STUB: 'FUTURE_CRAWLER_STUB', -} as const -export type DataSourceType = typeof DataSourceType[keyof typeof DataSourceType] - -export const DATA_SOURCE_TYPE_LABELS: Record = { - API_CONNECTOR: 'API-Connector', - CSV_IMPORT: 'CSV-Import', - MANUAL_UPLOAD: 'Manueller Upload', - PUBLIC_WEB_SOURCE: 'Öffentliche Web-Quelle', - PARTNER_FEED: 'Partner-Feed', - INTERNAL_PORTFOLIO_EXPORT: 'Portfolio-Export', - CONTRACT_METADATA_IMPORT: 'Vertrags-Import', - ANALYST_ENTRY: 'Analysten-Eingabe', - FUTURE_CRAWLER_STUB: 'Crawler (geplant)', -} - -// ── Source Status ───────────────────────────────────────────────────────────── -export const SourceStatus = { - ACTIVE: 'ACTIVE', - PAUSED: 'PAUSED', - ERROR: 'ERROR', - PENDING_REVIEW: 'PENDING_REVIEW', - DISABLED: 'DISABLED', -} as const -export type SourceStatus = typeof SourceStatus[keyof typeof SourceStatus] - -export const SOURCE_STATUS_LABELS: Record = { - ACTIVE: 'Aktiv', - PAUSED: 'Pausiert', - ERROR: 'Fehler', - PENDING_REVIEW: 'Prüfung ausstehend', - DISABLED: 'Deaktiviert', -} - -export const SOURCE_STATUS_COLORS: Record = { - ACTIVE: { bg: 'rgba(22,163,74,0.1)', fg: '#15803d' }, - PAUSED: { bg: 'rgba(234,179,8,0.1)', fg: '#a16207' }, - ERROR: { bg: 'rgba(239,68,68,0.1)', fg: '#dc2626' }, - PENDING_REVIEW: { bg: 'rgba(59,130,246,0.1)', fg: '#1d4ed8' }, - DISABLED: { bg: 'rgba(148,163,184,0.1)', fg: '#64748b' }, -} - -// ── Terms / Legal Status ────────────────────────────────────────────────────── -export const TermsStatus = { - APPROVED: 'APPROVED', - NEEDS_LEGAL_REVIEW: 'NEEDS_LEGAL_REVIEW', - RESTRICTED: 'RESTRICTED', - BLOCKED: 'BLOCKED', - UNKNOWN: 'UNKNOWN', -} as const -export type TermsStatus = typeof TermsStatus[keyof typeof TermsStatus] - -export const TERMS_STATUS_LABELS: Record = { - APPROVED: 'Genehmigt', - NEEDS_LEGAL_REVIEW: 'Rechtliche Prüfung', - RESTRICTED: 'Eingeschränkt', - BLOCKED: 'Gesperrt', - UNKNOWN: 'Unbekannt', -} - -export const TERMS_STATUS_COLORS: Record = { - APPROVED: { bg: 'rgba(22,163,74,0.08)', fg: '#15803d', border: 'rgba(22,163,74,0.3)' }, - NEEDS_LEGAL_REVIEW: { bg: 'rgba(234,179,8,0.08)', fg: '#a16207', border: 'rgba(234,179,8,0.3)' }, - RESTRICTED: { bg: 'rgba(249,115,22,0.08)', fg: '#c2410c', border: 'rgba(249,115,22,0.3)' }, - BLOCKED: { bg: 'rgba(239,68,68,0.08)', fg: '#dc2626', border: 'rgba(239,68,68,0.3)' }, - UNKNOWN: { bg: 'rgba(148,163,184,0.08)', fg: '#64748b', border: 'rgba(148,163,184,0.3)' }, -} - -// ── Connector Run Status ────────────────────────────────────────────────────── -export const ConnectorRunStatus = { - RUNNING: 'RUNNING', - COMPLETED: 'COMPLETED', - FAILED: 'FAILED', - PARTIAL: 'PARTIAL', - CANCELLED: 'CANCELLED', -} as const -export type ConnectorRunStatus = typeof ConnectorRunStatus[keyof typeof ConnectorRunStatus] - -export const CONNECTOR_RUN_STATUS_LABELS: Record = { - RUNNING: 'Läuft', - COMPLETED: 'Abgeschlossen', - FAILED: 'Fehlgeschlagen', - PARTIAL: 'Teilweise', - CANCELLED: 'Abgebrochen', -} - -export const CONNECTOR_RUN_STATUS_COLORS: Record = { - RUNNING: { bg: 'rgba(99,102,241,0.1)', fg: '#4f46e5' }, - COMPLETED: { bg: 'rgba(22,163,74,0.1)', fg: '#15803d' }, - FAILED: { bg: 'rgba(239,68,68,0.1)', fg: '#dc2626' }, - PARTIAL: { bg: 'rgba(234,179,8,0.1)', fg: '#a16207' }, - CANCELLED: { bg: 'rgba(148,163,184,0.1)', fg: '#64748b' }, -} - -// ── Interfaces ──────────────────────────────────────────────────────────────── -export interface DataSource { - id: string - name: string - sourceType: DataSourceType - ownerOrganizationId?: string - legalBasis: string - termsStatus: TermsStatus - dataCategories: string[] - supportedAssetTypes: string[] - regionCoverage: string[] - reliabilityScore: number - freshnessStatus: FreshnessStatus - lastRunAt?: string - nextRunAt?: string - status: SourceStatus - errorState?: string - notes?: string -} - -export interface ConnectorRun { - id: string - sourceId: string - startedAt: string - finishedAt?: string - status: ConnectorRunStatus - itemsDetected: number - itemsNormalized: number - itemsRejected: number - signalsCreated: number - errors: string[] - warnings: string[] - runSummary: string -} - -export interface SourceFilters { - search?: string - sourceType?: DataSourceType - status?: SourceStatus - termsStatus?: TermsStatus -} diff --git a/.claude/worktrees/agent-a82a3716/src/domain/enums.ts b/.claude/worktrees/agent-a82a3716/src/domain/enums.ts deleted file mode 100644 index 67a7d64..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/enums.ts +++ /dev/null @@ -1,172 +0,0 @@ -// ── Asset Types ─────────────────────────────────────────────────────────────── -export const AssetType = { - OFFICE: 'OFFICE', - RETAIL: 'RETAIL', - GASTRO: 'GASTRO', // legacy – kept for mock-data compatibility - LIGHT_INDUSTRIAL: 'LIGHT_INDUSTRIAL', - LOGISTICS: 'LOGISTICS', - PRODUCTION: 'PRODUCTION', - MIXED: 'MIXED', - UNKNOWN: 'UNKNOWN', -} as const -export type AssetType = typeof AssetType[keyof typeof AssetType] - -// ── Result Types ────────────────────────────────────────────────────────────── -export const ResultType = { - VERIFIED_PORTFOLIO: 'VERIFIED_PORTFOLIO', - EXTERNAL_MARKET: 'EXTERNAL_MARKET', - FUTURE_AVAILABILITY: 'FUTURE_AVAILABILITY', -} as const -export type ResultType = typeof ResultType[keyof typeof ResultType] - -// ── Match Strength ───────────────────────────────────────────────────────────── -export const MatchStrength = { - STRONG: 'STRONG', - MODERATE: 'MODERATE', - WEAK: 'WEAK', -} as const -export type MatchStrength = typeof MatchStrength[keyof typeof MatchStrength] - -// ── Match Status ────────────────────────────────────────────────────────────── -export const MatchStatus = { - PENDING_REVIEW: 'PENDING_REVIEW', - APPROVED: 'APPROVED', - REJECTED: 'REJECTED', - SHORTLISTED: 'SHORTLISTED', - ARCHIVED: 'ARCHIVED', -} as const -export type MatchStatus = typeof MatchStatus[keyof typeof MatchStatus] - -// ── Risk Level ──────────────────────────────────────────────────────────────── -export const RiskLevel = { - LOW: 'LOW', - MEDIUM: 'MEDIUM', - HIGH: 'HIGH', - CRITICAL: 'CRITICAL', -} as const -export type RiskLevel = typeof RiskLevel[keyof typeof RiskLevel] - -// ── Confidence Level (qualitative) ─────────────────────────────────────────── -export const ConfidenceLevel = { - VERY_HIGH: 'VERY_HIGH', // >= 0.90 - HIGH: 'HIGH', // >= 0.75 - MEDIUM: 'MEDIUM', // >= 0.55 - LOW: 'LOW', // >= 0.35 - VERY_LOW: 'VERY_LOW', // < 0.35 -} as const -export type ConfidenceLevel = typeof ConfidenceLevel[keyof typeof ConfidenceLevel] - -// ── Data Quality Level (qualitative) ───────────────────────────────────────── -export const DataQualityLevel = { - HIGH: 'HIGH', // >= 0.80 - MEDIUM: 'MEDIUM', // >= 0.60 - LOW: 'LOW', // < 0.60 - INCOMPLETE: 'INCOMPLETE', // missing critical fields -} as const -export type DataQualityLevel = typeof DataQualityLevel[keyof typeof DataQualityLevel] - -// ── Availability Status ─────────────────────────────────────────────────────── -export const AvailabilityStatus = { - AVAILABLE_NOW: 'AVAILABLE_NOW', - AVAILABLE_SOON: 'AVAILABLE_SOON', - FUTURE_SIGNAL: 'FUTURE_SIGNAL', - OCCUPIED: 'OCCUPIED', - UNKNOWN: 'UNKNOWN', -} as const -export type AvailabilityStatus = typeof AvailabilityStatus[keyof typeof AvailabilityStatus] - -// ── Availability Type (structural distinction) ──────────────────────────────── -export const AvailabilityType = { - CONFIRMED: 'CONFIRMED', // verified, date known - INDICATIVE: 'INDICATIVE', // external listing, unconfirmed - PROBABILISTIC: 'PROBABILISTIC', // AI signal, no confirmed date -} as const -export type AvailabilityType = typeof AvailabilityType[keyof typeof AvailabilityType] - -// ── Source Type ─────────────────────────────────────────────────────────────── -export const SourceType = { - ERP_IMPORT: 'ERP_IMPORT', - MANUAL_ENTRY: 'MANUAL_ENTRY', - IMMOSCOUT_SCRAPE: 'IMMOSCOUT_SCRAPE', - HOMEGATE_SCRAPE: 'HOMEGATE_SCRAPE', - NEWHOME_SCRAPE: 'NEWHOME_SCRAPE', - MATCHOFFICE_SCRAPE: 'MATCHOFFICE_SCRAPE', - MAISON_WORK_SCRAPE: 'MAISON_WORK_SCRAPE', - AI_SIGNAL: 'AI_SIGNAL', - PARTNER_FEED: 'PARTNER_FEED', - UNKNOWN: 'UNKNOWN', -} as const -export type SourceType = typeof SourceType[keyof typeof SourceType] - -// ── Freshness Status ────────────────────────────────────────────────────────── -export const FreshnessStatus = { - FRESH: 'FRESH', // updated within 48h - STALE: 'STALE', // 2–14 days old - OUTDATED: 'OUTDATED', // > 14 days old -} as const -export type FreshnessStatus = typeof FreshnessStatus[keyof typeof FreshnessStatus] - -/** @deprecated Use FreshnessStatus — kept for mock-data backward compatibility */ -export const DataFreshness = FreshnessStatus -export type DataFreshness = FreshnessStatus - -// ── Review Status ───────────────────────────────────────────────────────────── -export const ReviewStatus = { - UNREVIEWED: 'UNREVIEWED', - IN_REVIEW: 'IN_REVIEW', - APPROVED: 'APPROVED', - REJECTED: 'REJECTED', - FLAGGED: 'FLAGGED', -} as const -export type ReviewStatus = typeof ReviewStatus[keyof typeof ReviewStatus] - -// ── Sensitivity Level ───────────────────────────────────────────────────────── -export const SensitivityLevel = { - PUBLIC: 'PUBLIC', - INTERNAL: 'INTERNAL', - CONFIDENTIAL: 'CONFIDENTIAL', - RESTRICTED: 'RESTRICTED', -} as const -export type SensitivityLevel = typeof SensitivityLevel[keyof typeof SensitivityLevel] - -// ── Shortlist Status ────────────────────────────────────────────────────────── -export const ShortlistStatus = { - DRAFT: 'DRAFT', - REVIEW_READY: 'REVIEW_READY', - FINALIZED: 'FINALIZED', - ACTIVE: 'ACTIVE', - SHARED: 'SHARED', - ARCHIVED: 'ARCHIVED', - CONVERTED: 'CONVERTED', -} as const -export type ShortlistStatus = typeof ShortlistStatus[keyof typeof ShortlistStatus] - -// ── User Role ───────────────────────────────────────────────────────────────── -export const UserRole = { - SUPER_ADMIN: 'SUPER_ADMIN', - ORGANIZATION_ADMIN: 'ORGANIZATION_ADMIN', - PROPERTY_MANAGER: 'PROPERTY_MANAGER', - REVIEWER: 'REVIEWER', - OWNER_VIEWER: 'OWNER_VIEWER', - DEMAND_USER: 'DEMAND_USER', -} as const -export type UserRole = typeof UserRole[keyof typeof UserRole] - -// ── Signal Type ─────────────────────────────────────────────────────────────── -export const SignalType = { - EXPANSION: 'EXPANSION', - POSSIBLE_MOVE_OUT: 'POSSIBLE_MOVE_OUT', - CONSTRUCTION_PROJECT: 'CONSTRUCTION_PROJECT', - RESTRUCTURING: 'RESTRUCTURING', - PROJECT_DEVELOPMENT: 'PROJECT_DEVELOPMENT', - SPACE_CONSOLIDATION: 'SPACE_CONSOLIDATION', -} as const -export type SignalType = typeof SignalType[keyof typeof SignalType] - -// ── Workspace ───────────────────────────────────────────────────────────────── -export const WorkspaceType = { - SUPPLY: 'SUPPLY', - DEMAND: 'DEMAND', - OPERATIONS: 'OPERATIONS', -} as const -export type WorkspaceType = typeof WorkspaceType[keyof typeof WorkspaceType] diff --git a/.claude/worktrees/agent-a82a3716/src/domain/futureSignal.ts b/.claude/worktrees/agent-a82a3716/src/domain/futureSignal.ts deleted file mode 100644 index 57488e7..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/futureSignal.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { SignalType, RiskLevel, ReviewStatus } 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 SignalEvidence { - summary: string - sourceUrls?: string[] - extractedAt?: string -} - -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 - - // F004 additions - title?: string // short human-readable headline - evidence?: SignalEvidence // structured evidence block - matchabilityScore?: number // 0–100: how well this signal can be matched to needs - reviewStatus?: ReviewStatus -} diff --git a/.claude/worktrees/agent-a82a3716/src/domain/index.ts b/.claude/worktrees/agent-a82a3716/src/domain/index.ts deleted file mode 100644 index 200c277..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './enums' -export * from './property' -export * from './need' -export * from './match' -export * from './futureSignal' -export * from './aiOutput' -export * from './activityEvent' -export * from './unifiedResult' -export * from './shortlist' -export * from './review' diff --git a/.claude/worktrees/agent-a82a3716/src/domain/marketSignal.ts b/.claude/worktrees/agent-a82a3716/src/domain/marketSignal.ts deleted file mode 100644 index d45822d..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/marketSignal.ts +++ /dev/null @@ -1,180 +0,0 @@ -import type { AssetType, SignalType, SensitivityLevel, FreshnessStatus } from './enums' - -// ── Source Categories ───────────────────────────────────────────────────────── - -export const MarketSignalSourceCategory = { - PUBLIC_LISTING_PLATFORM: 'PUBLIC_LISTING_PLATFORM', - BUILDING_PERMIT_REGISTER: 'BUILDING_PERMIT_REGISTER', - COMPANY_NEWS: 'COMPANY_NEWS', - JOB_GROWTH_SIGNAL: 'JOB_GROWTH_SIGNAL', - COMMERCIAL_REGISTER: 'COMMERCIAL_REGISTER', - INFRASTRUCTURE_PROJECT: 'INFRASTRUCTURE_PROJECT', - PORTFOLIO_IMPORT: 'PORTFOLIO_IMPORT', - LEASE_EXPIRY_DATA: 'LEASE_EXPIRY_DATA', - USER_DEMAND_SIGNAL: 'USER_DEMAND_SIGNAL', - MANUAL_ANALYST_SIGNAL: 'MANUAL_ANALYST_SIGNAL', -} as const -export type MarketSignalSourceCategory = typeof MarketSignalSourceCategory[keyof typeof MarketSignalSourceCategory] - -export const MARKET_SIGNAL_SOURCE_LABELS: Record = { - PUBLIC_LISTING_PLATFORM: 'Listing-Plattform', - BUILDING_PERMIT_REGISTER: 'Baugesuch-Register', - COMPANY_NEWS: 'Unternehmensnachrichten', - JOB_GROWTH_SIGNAL: 'Stellenwachstum', - COMMERCIAL_REGISTER: 'Handelsregister', - INFRASTRUCTURE_PROJECT: 'Infrastrukturprojekt', - PORTFOLIO_IMPORT: 'Portfolio-Import', - LEASE_EXPIRY_DATA: 'Vertragslaufdaten', - USER_DEMAND_SIGNAL: 'Nutzernachfrage', - MANUAL_ANALYST_SIGNAL: 'Analyst-Signal', -} - -// LEASE_EXPIRY_DATA and PORTFOLIO_IMPORT are sensitive — never expose raw to Demand Users -export const SENSITIVE_SOURCE_CATEGORIES: MarketSignalSourceCategory[] = [ - MarketSignalSourceCategory.LEASE_EXPIRY_DATA, - MarketSignalSourceCategory.PORTFOLIO_IMPORT, -] - -// ── Processing Status ───────────────────────────────────────────────────────── - -export const SignalProcessingStatus = { - DETECTED: 'DETECTED', - NORMALIZED: 'NORMALIZED', - ENRICHED: 'ENRICHED', - NEEDS_REVIEW: 'NEEDS_REVIEW', - APPROVED_AS_SIGNAL: 'APPROVED_AS_SIGNAL', - REJECTED: 'REJECTED', - CONVERTED_TO_FUTURE_AVAILABILITY: 'CONVERTED_TO_FUTURE_AVAILABILITY', - ARCHIVED: 'ARCHIVED', -} as const -export type SignalProcessingStatus = typeof SignalProcessingStatus[keyof typeof SignalProcessingStatus] - -export const SIGNAL_PROCESSING_STATUS_LABELS: Record = { - DETECTED: 'Erkannt', - NORMALIZED: 'Normalisiert', - ENRICHED: 'Angereichert', - NEEDS_REVIEW: 'Prüfung erforderlich', - APPROVED_AS_SIGNAL: 'Genehmigt', - REJECTED: 'Abgelehnt', - CONVERTED_TO_FUTURE_AVAILABILITY: 'Konvertiert', - ARCHIVED: 'Archiviert', -} - -export const SIGNAL_PROCESSING_STATUS_COLORS: Record = { - DETECTED: { bg: 'rgba(148,163,184,0.15)', fg: '#64748b' }, - NORMALIZED: { bg: 'rgba(59,130,246,0.12)', fg: '#2563eb' }, - ENRICHED: { bg: 'rgba(99,102,241,0.12)', fg: '#4f46e5' }, - NEEDS_REVIEW: { bg: 'rgba(245,158,11,0.12)', fg: '#d97706' }, - APPROVED_AS_SIGNAL: { bg: 'rgba(34,197,94,0.12)', fg: '#16a34a' }, - REJECTED: { bg: 'rgba(239,68,68,0.12)', fg: '#dc2626' }, - CONVERTED_TO_FUTURE_AVAILABILITY: { bg: 'rgba(139,92,246,0.12)', fg: '#7c3aed' }, - ARCHIVED: { bg: 'rgba(148,163,184,0.10)', fg: '#94a3b8' }, -} - -// ── Evidence & Entities ─────────────────────────────────────────────────────── - -export const ExtractedEntityType = { - COMPANY: 'COMPANY', - PERSON: 'PERSON', - LOCATION: 'LOCATION', - ASSET: 'ASSET', - DATE: 'DATE', -} as const -export type ExtractedEntityType = typeof ExtractedEntityType[keyof typeof ExtractedEntityType] - -export interface ExtractedEntity { - type: ExtractedEntityType - value: string - confidence: number -} - -export const EvidenceType = { - TEXT_EXCERPT: 'TEXT_EXCERPT', - URL_REFERENCE: 'URL_REFERENCE', - ANALYST_NOTE: 'ANALYST_NOTE', - DOCUMENT: 'DOCUMENT', -} as const -export type EvidenceType = typeof EvidenceType[keyof typeof EvidenceType] - -export interface SignalEvidence { - id: string - signalId: string - evidenceType: EvidenceType - content: string - sourceUrl?: string - retrievedAt: string - confidence: number -} - -// ── Core Signal ─────────────────────────────────────────────────────────────── - -export interface MarketSignal { - id: string - title: string - summary: string - sourceCategory: MarketSignalSourceCategory - sourceLabel: string - sourceUrl?: string - detectedAt: string - location: string - affectedAssetTypes: AssetType[] - signalType: SignalType - rawEvidenceSummary: string - extractedEntities: ExtractedEntity[] - evidence: SignalEvidence[] - sourceReliabilityScore: number // 0–1 - confidenceScore: number // 0–1 - sensitivityLevel: SensitivityLevel - freshnessStatus: FreshnessStatus - processingStatus: SignalProcessingStatus - linkedPropertyId?: string - linkedNeedId?: string - possibleFutureSignalId?: string - analystNotes: string - createdAt: string - updatedAt: string -} - -// ── Intelligence Aggregates ─────────────────────────────────────────────────── - -export interface MarketInsight { - id: string - title: string - summary: string - signalIds: string[] - location: string - affectedAssetTypes: AssetType[] - confidenceScore: number - createdAt: string - analystId: string -} - -export interface IntelligenceRun { - id: string - triggeredAt: string - completedAt?: string - signalsDetected: number - signalsProcessed: number - status: 'RUNNING' | 'COMPLETED' | 'FAILED' - sourceCategories: MarketSignalSourceCategory[] -} - -export interface SignalConversionCandidate { - signalId: string - proposedTitle: string - proposedSummary: string - estimatedAvailabilityDate?: string - proposedConfidence: number - conversionRationale: string - requiresReview: boolean -} - -// ── Filters ─────────────────────────────────────────────────────────────────── - -export interface MarketSignalFilters { - sourceCategory?: MarketSignalSourceCategory - processingStatus?: SignalProcessingStatus - sensitivityLevel?: SensitivityLevel - signalType?: SignalType - search?: string -} diff --git a/.claude/worktrees/agent-a82a3716/src/domain/match.ts b/.claude/worktrees/agent-a82a3716/src/domain/match.ts deleted file mode 100644 index 2b5af59..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/match.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { MatchStrength, MatchStatus, RiskLevel, ResultType, ConfidenceLevel } from './enums' - -// ── Score Building Blocks ───────────────────────────────────────────────────── - -export interface ScoreBreakdown { - hardMatchScore: number // 0–100 hard criteria score - softFactorScore: number // 0–100 soft factors score - confidenceModifier: number // -20 to +5 adjustment - dataQualityModifier: number // -15 to 0 adjustment - totalScore: number // final 0–100 -} - -export interface ScoreFactor { - criterion: string - weight: number // 0–1 relative weight - score: number // 0–100 - contribution: number // weighted points added to total - explanation: string -} - -// ── Explainability Types ────────────────────────────────────────────────────── - -/** Canonical tradeoff type (F004) */ -export interface TradeOff { - criterion: string - concern: string - severity: 'LOW' | 'MEDIUM' | 'HIGH' - mitigation?: string - impactOnScore?: number -} - -/** @deprecated Use TradeOff — kept for backward compatibility */ -export type Tradeoff = TradeOff - -export interface Risk { - category: string // e.g. "Datenverfügbarkeit", "Standortrisiko" - description: string - level: RiskLevel - mitigation?: string -} - -export interface MissingDataItem { - field: string - importance: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' - description: string - impact: string // how it affects the match score -} - -export interface NextBestAction { - label: string // e.g. "Besichtigung anfragen" - description?: string - priority: 'HIGH' | 'MEDIUM' | 'LOW' - actionType: 'CONTACT' | 'VERIFY' | 'REVIEW' | 'SHORTLIST' | 'COMPARE' | 'SCHEDULE' - externalUrl?: string -} - -export interface AlternativeStrategy { - title: string - description: string - expectedScore?: number - reasoning?: string -} - -// ── Match (core entity) ─────────────────────────────────────────────────────── - -export interface Match { - id: string - needId: string - - // Result reference — works for all three result types - resultId?: string // preferred: generic result ID - resultType?: ResultType - propertyId: string // legacy alias for resultId (VERIFIED_PORTFOLIO) - - // Scoring - matchScore: number // 0–100 - matchStrength: MatchStrength - scoreBreakdown: ScoreBreakdown - confidenceLevel: number // 0–1 numeric - confidenceLevelLabel?: ConfidenceLevel - dataConfidenceScore?: number // 0–1 data quality contribution - - // Explainability - positiveFactors: ScoreFactor[] - negativeFactors: ScoreFactor[] - tradeoffs: TradeOff[] - tradeOffs?: TradeOff[] // alias for F004 naming convention - risks?: Risk[] - missingData?: MissingDataItem[] - nextBestActions?: NextBestAction[] - explainabilitySummary: string - explanation?: string // alias / longer form - - // Risk - riskLevel: RiskLevel - uncertaintyIndicators: string[] - - // Alternatives - alternativeStrategies?: AlternativeStrategy[] - - // Review / lifecycle - status?: MatchStatus - isApproved?: boolean // legacy — prefer status - reviewedBy?: string - reviewedAt?: string - organizationId?: string - createdAt: string - updatedAt: string -} diff --git a/.claude/worktrees/agent-a82a3716/src/domain/need.ts b/.claude/worktrees/agent-a82a3716/src/domain/need.ts deleted file mode 100644 index 9ffcbab..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/need.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { AssetType, MatchStatus } 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 SizeRange { - minSqm: number - maxSqm: number -} - -export interface MustHaveCriterion { - criterion: string - description?: string - weight?: number // 0–1, how much a miss hurts the score -} - -export interface WeightedPreference { - criterion: string - weight: number // 0–1 - idealValue?: string | number - description?: string -} - -export interface Need { - id: string - companyName: string - contactName?: string - assetType: AssetType - requiredArea: AreaRange - - // F004 additions - desiredLocation?: string[] // preferred city/district list - sizeRange?: SizeRange // structured alias for requiredArea - mustHaveCriteria?: MustHaveCriterion[] - weightedPreferences?: WeightedPreference[] - status?: MatchStatus | 'ACTIVE' | 'INACTIVE' | 'DRAFT' | 'CLOSED' - - preferredLocations: string[] - excludedLocations?: string[] - budgetRange: BudgetRange - timing: Timing - mustCriteriaText?: string[] // legacy — prefer mustHaveCriteria - 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/.claude/worktrees/agent-a82a3716/src/domain/needBuilder.ts b/.claude/worktrees/agent-a82a3716/src/domain/needBuilder.ts deleted file mode 100644 index 07a5ddd..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/needBuilder.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { AssetType } from './enums' - -export interface ParsedNeedCriteria { - assetType?: AssetType - areaRange?: { min: number; max: number } - preferredLocations?: string[] - budgetRange?: { maxPerSqm: number; maxMonthlyTotal?: number; currency: string } - timing?: { earliestMoveIn: string; latestMoveIn?: string; contractDurationMonths?: number; flexibleTiming: boolean } - mustHaveCriteria?: string[] - softFactors?: { minPrestige?: number; requireParking?: boolean; maxPublicTransportMinutes?: number; requireHighVisibility?: boolean } - infrastructureRequirements?: string[] - accessibilityRequirements?: string[] - prestigeImportance?: 'LOW' | 'MEDIUM' | 'HIGH' - flexibilityNeed?: 'LOW' | 'MEDIUM' | 'HIGH' - expansionPotential?: boolean - parkingNeed?: boolean - visibilityNeed?: 'LOW' | 'MEDIUM' | 'HIGH' - footfallNeed?: 'LOW' | 'MEDIUM' | 'HIGH' - companyName?: string - notes?: string -} - -export interface FollowUpQuestion { - id: string - questionText: string - targetField: string - reason: string - suggestedAnswerOptions?: string[] - importance: 'required' | 'recommended' | 'optional' -} - -export interface ParseNeedResult { - extractedCriteria: ParsedNeedCriteria - confidenceByField: Record - missingFields: string[] - assumptions: string[] - suggestedWeights: Record - followUpQuestionCandidates: FollowUpQuestion[] - rawSummary: string - promptVersion: string - schemaVersion: string -} - -export const NeedBuilderStep = { - IDLE: 'idle', - PARSING: 'parsing', - PARSED_REQUIRES_REVIEW: 'parsed_requires_review', - CLARIFICATION_REQUIRED: 'clarification_required', - WEIGHTING_REVIEW: 'weighting_review', - READY_TO_SAVE: 'ready_to_save', - SAVING: 'saving', - SAVED: 'saved', - ERROR: 'error', -} as const -export type NeedBuilderStep = typeof NeedBuilderStep[keyof typeof NeedBuilderStep] - -export const WEIGHTING_KEYS = ['area', 'location', 'budget', 'timing', 'prestige', 'accessibility', 'expansionPotential', 'flexibility'] as const -export type WeightingKey = typeof WEIGHTING_KEYS[number] - -export const WEIGHTING_LABELS: Record = { - area: 'Fläche', - location: 'Standort', - budget: 'Budget', - timing: 'Verfügbarkeit', - prestige: 'Prestige', - accessibility: 'Erreichbarkeit', - expansionPotential: 'Expansionspotenzial', - flexibility: 'Flexibilität', -} diff --git a/.claude/worktrees/agent-a82a3716/src/domain/property.ts b/.claude/worktrees/agent-a82a3716/src/domain/property.ts deleted file mode 100644 index 02c7948..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/property.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { - AssetType, ResultType, AvailabilityStatus, AvailabilityType, - FreshnessStatus, RiskLevel, SourceType, DataQualityLevel, -} from './enums' - -// ── Location / Address ──────────────────────────────────────────────────────── - -export interface Location { - city: string - district?: string - canton?: string - region?: string - country: string - coordinates?: { lat: number; lng: number } -} - -export interface Address { - street: string - houseNumber: string - postalCode: string - city: string - country: string -} - -// ── Source Metadata ─────────────────────────────────────────────────────────── - -export interface SourceMeta { - sourceType: SourceType | string - sourceLabel?: string - sourceUrl?: string - sourceUpdatedAt?: string - externalId?: string -} - -// ── Data Quality ────────────────────────────────────────────────────────────── - -export interface DataQuality { - score: number - qualityLevel?: DataQualityLevel - missingCriticalFields: string[] - missingOptionalFields: string[] - lastVerifiedAt?: string - freshness: FreshnessStatus - warnings: string[] -} - -// ── Hard Facts ──────────────────────────────────────────────────────────────── - -export interface PropertyHardFacts { - floor?: number - fitOut?: 'SHELL' | 'BASIC' | 'FULL' | 'PREMIUM' - parking?: number - publicTransportScore?: number - usageType?: string - ceilingHeightM?: number - loadingDocksCount?: number - powerSupplyKva?: number - hasServerRoom?: boolean - isBarrierFree?: boolean -} - -// ── Soft Factors ────────────────────────────────────────────────────────────── - -export interface SoftFactors { - prestigeScore?: number - visibilityScore?: number - footfallScore?: number - commuterAccessScore?: number - talentAccessScore?: number - esgScore?: number - flexibilityScore?: number - expansionPotentialScore?: number - taxEnvironmentScore?: number - // Legacy aliases kept for mock-data backward compatibility - prestige?: number - accessibility?: number - talentAccess?: number - esgRating?: string - passerbyFrequency?: 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH' - parkingSpots?: number - publicTransportMinutes?: number - infrastructureNotes?: string -} - -// ── Property ────────────────────────────────────────────────────────────────── - -export interface Property { - id: string - organizationId?: string - title: string - - assetType: AssetType - resultType: ResultType - - location: Location - address: Address - - areaSqm: number - areaSqmMin?: number - areaSqmMax?: number - - rentPricePerSqm: number - rentChfSqmYear?: number - totalRentMonthly?: number - ancillaryCosts?: number - - availabilityDate: string - availabilityStatus: AvailabilityStatus - availabilityType?: AvailabilityType - - sourceType: string - sourceLabel?: string - sourceUrl?: string - sourceUpdatedAt?: string - sourceMeta?: SourceMeta - - confidenceScore: number - dataQuality: DataQuality - - softFactors?: SoftFactors - hardFacts?: PropertyHardFacts - - // Legacy fields — kept for backward compat - floorLevel?: number - expansionPotentialSqm?: number - - contractDurationMonths?: number - riskLevel?: RiskLevel - description?: string - images?: string[] - - status?: 'ACTIVE' | 'INACTIVE' | 'DRAFT' | 'ARCHIVED' - lastReviewedAt?: string - createdAt: string - updatedAt: string -} - -export type CreatePropertyInput = Omit -export type UpdatePropertyInput = Partial diff --git a/.claude/worktrees/agent-a82a3716/src/domain/review.ts b/.claude/worktrees/agent-a82a3716/src/domain/review.ts deleted file mode 100644 index 5ade373..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/review.ts +++ /dev/null @@ -1,79 +0,0 @@ -// ── Entity Types ────────────────────────────────────────────────────────────── - -export const ReviewEntityType = { - FUTURE_SIGNAL: 'FUTURE_SIGNAL', - MATCH_EXPLANATION: 'MATCH_EXPLANATION', - LOW_CONFIDENCE_MATCH:'LOW_CONFIDENCE_MATCH', - CONTACT_RELEASE: 'CONTACT_RELEASE', - AI_OUTPUT: 'AI_OUTPUT', - PROPERTY_DATA_ISSUE: 'PROPERTY_DATA_ISSUE', -} as const -export type ReviewEntityType = typeof ReviewEntityType[keyof typeof ReviewEntityType] - -// ── Task Status ─────────────────────────────────────────────────────────────── - -export const ReviewTaskStatus = { - PENDING: 'PENDING', - IN_REVIEW: 'IN_REVIEW', - APPROVED: 'APPROVED', - REJECTED: 'REJECTED', - NEEDS_MORE_DATA: 'NEEDS_MORE_DATA', - ESCALATED: 'ESCALATED', -} as const -export type ReviewTaskStatus = typeof ReviewTaskStatus[keyof typeof ReviewTaskStatus] - -// ── Priority ────────────────────────────────────────────────────────────────── - -export const ReviewPriority = { - LOW: 'LOW', - MEDIUM: 'MEDIUM', - HIGH: 'HIGH', - CRITICAL: 'CRITICAL', -} as const -export type ReviewPriority = typeof ReviewPriority[keyof typeof ReviewPriority] - -// ── Note ────────────────────────────────────────────────────────────────────── - -export interface ReviewNote { - id: string - content: string - createdBy: string - createdAt: string -} - -// ── Task ────────────────────────────────────────────────────────────────────── - -export interface ReviewTask { - id: string - entityType: ReviewEntityType - entityId: string - title: string - description?: string - priority: ReviewPriority - status: ReviewTaskStatus - assignedTo?: string - createdBy: string - createdAt: string - updatedAt: string - dueDate?: string - reviewNotes: ReviewNote[] - relatedOrganizationId?: string - // Risk / confidence context - confidenceScore?: number - riskLevel?: string - // AI-output context - promptVersion?: string - // Legacy compat (match-based items) - matchId?: string - needId?: string - propertyId?: string - matchScore?: number -} - -// ── Backward-compat aliases ─────────────────────────────────────────────────── - -export type ReviewQueueItem = ReviewTask -export type ReviewQueueStatus = ReviewTaskStatus - -/** @deprecated use ReviewTaskStatus */ -export const ReviewQueueStatus = ReviewTaskStatus diff --git a/.claude/worktrees/agent-a82a3716/src/domain/scoring.ts b/.claude/worktrees/agent-a82a3716/src/domain/scoring.ts deleted file mode 100644 index c2ee43f..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/scoring.ts +++ /dev/null @@ -1,143 +0,0 @@ -import type { ScoreFactor, TradeOff, Risk, MissingDataItem, NextBestAction } from './match' - -// ── Hard Filter Thresholds ──────────────────────────────────────────────────── - -export const HARD_FILTER = { - AREA_MIN_TOLERANCE: 0.85, // exclude if property < 85% of need's min area - AREA_MAX_RATIO: 2.50, // severe penalty if property > 2.5× need's max area - BUDGET_EXCLUSION_RATIO: 1.50, // exclude if rent > 150% of max budget/m² - BUDGET_SEVERE_RATIO: 1.25, // severe penalty if 125–150% over budget - BUDGET_MODERATE_RATIO: 1.10, // mild penalty if 110–125% over budget - TIMING_GRACE_DAYS: 90, // allow ±90 days window flexibility -} as const - -// ── Score Architecture ──────────────────────────────────────────────────────── - -// Within each group, scores are weighted and normalized to 0–100. -// Final = baseScore + dataQualityModifier + confidenceModifier, clamped 0–100. -export const SCORE_SPLIT = { - HARD_CRITERIA: 0.60, // expected contribution from hard criteria group - SOFT_FACTORS: 0.40, // expected contribution from soft factors group -} as const - -export const HARD_CRITERION_KEYS = ['area', 'location', 'budget', 'timing'] as const -export type HardCriterionKey = typeof HARD_CRITERION_KEYS[number] - -export const SOFT_FACTOR_KEYS = [ - 'prestige', 'accessibility', 'expansionPotential', 'flexibility', - 'visibility', 'footfall', 'talentAccess', 'esg', 'taxEnvironment', -] as const -export type SoftFactorKey = typeof SOFT_FACTOR_KEYS[number] - -// ── Modifier Tables ─────────────────────────────────────────────────────────── - -export const DATA_QUALITY_MODIFIER = { - EXCELLENT: +5, // dataQuality.score >= 0.85 - GOOD: 0, // >= 0.70 - FAIR: -5, // >= 0.55 - POOR: -10, // >= 0.40 - CRITICAL: -15, // < 0.40 -} as const - -export const CONFIDENCE_MODIFIER = { - VERIFIED_HIGH: +3, // VERIFIED_PORTFOLIO + confidenceScore >= 0.80 - VERIFIED_MEDIUM: 0, // VERIFIED_PORTFOLIO + confidenceScore < 0.80 - EXTERNAL_MARKET: -3, // EXTERNAL_MARKET result type - FUTURE_AVAILABILITY: -15, // FUTURE_AVAILABILITY — never treat as confirmed availability - LOW_CONFIDENCE: -10, // confidenceScore < 0.50 (stacks with above) -} as const - -// ── Scoring Weight Profile ──────────────────────────────────────────────────── - -export interface ScoringWeightProfile { - // Hard criteria - area: number - location: number - budget: number - timing: number - // Soft factors - prestige: number - accessibility: number - expansionPotential: number - flexibility: number - visibility: number - footfall: number - talentAccess: number - esg: number - taxEnvironment: number - [key: string]: number -} - -// ── Default Profiles per Asset Type ────────────────────────────────────────── -// Each profile sums to 1.00. No magic numbers — weights reflect domain logic. - -export const DEFAULT_SCORING_PROFILES: Record = { - // Büro: ÖV-Anbindung, Talent Access, Prestige, ESG stark gewichtet - OFFICE: { - area: 0.18, location: 0.18, budget: 0.15, timing: 0.09, - prestige: 0.07, accessibility: 0.11, expansionPotential: 0.05, - flexibility: 0.05, visibility: 0.02, footfall: 0.01, - talentAccess: 0.07, esg: 0.02, taxEnvironment: 0.00, - }, - // Retail: Frequenz, Sichtbarkeit und Standort dominieren - RETAIL: { - area: 0.10, location: 0.15, budget: 0.13, timing: 0.06, - prestige: 0.04, accessibility: 0.07, expansionPotential: 0.03, - flexibility: 0.08, visibility: 0.14, footfall: 0.18, - talentAccess: 0.01, esg: 0.01, taxEnvironment: 0.00, - }, - // Light Industrial: Fläche, Andienung (accessibility), Infrastruktur - LIGHT_INDUSTRIAL: { - area: 0.20, location: 0.12, budget: 0.18, timing: 0.10, - prestige: 0.01, accessibility: 0.14, expansionPotential: 0.07, - flexibility: 0.05, visibility: 0.01, footfall: 0.00, - talentAccess: 0.04, esg: 0.04, taxEnvironment: 0.04, - }, - // Logistik: Autobahnanbindung (accessibility), Andienung, Fläche, Verfügbarkeit - LOGISTICS: { - area: 0.18, location: 0.18, budget: 0.14, timing: 0.13, - prestige: 0.01, accessibility: 0.18, expansionPotential: 0.06, - flexibility: 0.04, visibility: 0.01, footfall: 0.00, - talentAccess: 0.02, esg: 0.02, taxEnvironment: 0.03, - }, - PRODUCTION: { - area: 0.22, location: 0.13, budget: 0.18, timing: 0.10, - prestige: 0.01, accessibility: 0.13, expansionPotential: 0.08, - flexibility: 0.04, visibility: 0.01, footfall: 0.00, - talentAccess: 0.04, esg: 0.03, taxEnvironment: 0.03, - }, - DEFAULT: { - area: 0.20, location: 0.18, budget: 0.18, timing: 0.10, - prestige: 0.05, accessibility: 0.09, expansionPotential: 0.05, - flexibility: 0.05, visibility: 0.02, footfall: 0.02, - talentAccess: 0.03, esg: 0.02, taxEnvironment: 0.01, - }, -} - -// ── Engine IO Types ─────────────────────────────────────────────────────────── - -export interface HardFilterResult { - excluded: boolean - reason?: string - severePenalty: number // extra points deducted on top of criterion score (0–30) -} - -export interface MatchEngineOutput { - propertyId: string - needId: string - excluded: boolean - excludedReason?: string - finalScore: number // 0–100 clamped - hardMatchScore: number // 0–100 normalized - softFactorScore: number // 0–100 normalized - dataQualityModifier: number - confidenceModifier: number - positiveFactors: ScoreFactor[] - negativeFactors: ScoreFactor[] - allHardFactors: ScoreFactor[] - allSoftFactors: ScoreFactor[] - tradeOffs: TradeOff[] - risks: Risk[] - missingData: MissingDataItem[] - nextBestActions: NextBestAction[] -} diff --git a/.claude/worktrees/agent-a82a3716/src/domain/shortlist.ts b/.claude/worktrees/agent-a82a3716/src/domain/shortlist.ts deleted file mode 100644 index b6d2787..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/shortlist.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { ShortlistStatus } from './enums' - -export interface ShortlistItem { - resultId: string - resultType: string - title: string - matchScore: number - confidenceScore?: number - dataQualityScore?: number - sourceLabel?: string - addedAt: string - addedBy: string - note?: string - propertyId?: string -} - -export interface ShortlistItemInput { - resultId: string - resultType: string - title: string - matchScore: number - confidenceScore?: number - dataQualityScore?: number - sourceLabel?: string - addedBy: string - note?: string - propertyId?: string -} - -export interface Shortlist { - id: string - title: string - description?: string - needId?: string - ownerUserId?: string - decisionBriefId?: string - items: ShortlistItem[] - status: ShortlistStatus - createdBy: string - organizationId?: string - sharedWith?: string[] - createdAt: string - updatedAt: string -} - -export type CreateShortlistInput = Omit -export type UpdateShortlistInput = Partial diff --git a/.claude/worktrees/agent-a82a3716/src/domain/signalPipeline.ts b/.claude/worktrees/agent-a82a3716/src/domain/signalPipeline.ts deleted file mode 100644 index 267b996..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/signalPipeline.ts +++ /dev/null @@ -1,125 +0,0 @@ -// ── Pipeline Stages ─────────────────────────────────────────────────────────── -export const PipelineStage = { - STAGE_1_RAW_EVIDENCE: 'STAGE_1_RAW_EVIDENCE', - STAGE_2_NORMALIZED: 'STAGE_2_NORMALIZED', - STAGE_3_ENRICHED: 'STAGE_3_ENRICHED', - STAGE_4_REVIEW_CANDIDATE: 'STAGE_4_REVIEW_CANDIDATE', - STAGE_5_APPROVED_FUTURE: 'STAGE_5_APPROVED_FUTURE', - STAGE_6_MATCHABLE_RESULT: 'STAGE_6_MATCHABLE_RESULT', - STAGE_7_STRATEGIC_INPUT: 'STAGE_7_STRATEGIC_INPUT', -} as const -export type PipelineStage = typeof PipelineStage[keyof typeof PipelineStage] - -export const PIPELINE_STAGE_LABELS: Record = { - STAGE_1_RAW_EVIDENCE: 'Rohe Markt-Evidenz', - STAGE_2_NORMALIZED: 'Normalisiertes Signal', - STAGE_3_ENRICHED: 'Angereichertes Signal', - STAGE_4_REVIEW_CANDIDATE: 'Review-Kandidat', - STAGE_5_APPROVED_FUTURE: 'Genehmigtes Future Signal', - STAGE_6_MATCHABLE_RESULT: 'Matchbares Ergebnis', - STAGE_7_STRATEGIC_INPUT: 'Strategischer Entscheidungs-Input', -} - -export const PIPELINE_STAGE_DESCRIPTIONS: Record = { - STAGE_1_RAW_EVIDENCE: 'Rohe Evidenz aus Quellen gesammelt – noch unverarbeitet', - STAGE_2_NORMALIZED: 'Daten normalisiert, Felder validiert und vereinheitlicht', - STAGE_3_ENRICHED: 'Entitäten extrahiert, Kontext angereichert und bewertet', - STAGE_4_REVIEW_CANDIDATE: 'Signal bereit für manuellen Analyst-Review', - STAGE_5_APPROVED_FUTURE: 'Genehmigt als Future Availability Signal – intern sichtbar', - STAGE_6_MATCHABLE_RESULT: 'Im Unified Result Feed – Match-Engine nutzbar', - STAGE_7_STRATEGIC_INPUT: 'Eingang in Decision Briefs und strategische Analyse', -} - -export const PIPELINE_STAGE_ORDER: PipelineStage[] = [ - 'STAGE_1_RAW_EVIDENCE', - 'STAGE_2_NORMALIZED', - 'STAGE_3_ENRICHED', - 'STAGE_4_REVIEW_CANDIDATE', - 'STAGE_5_APPROVED_FUTURE', - 'STAGE_6_MATCHABLE_RESULT', - 'STAGE_7_STRATEGIC_INPUT', -] - -// ── Gate Types ──────────────────────────────────────────────────────────────── -export const GateType = { - EVIDENCE_GATE: 'EVIDENCE_GATE', - CONFIDENCE_GATE: 'CONFIDENCE_GATE', - SENSITIVITY_GATE: 'SENSITIVITY_GATE', - REVIEW_GATE: 'REVIEW_GATE', - MATCHABILITY_GATE: 'MATCHABILITY_GATE', - FEED_ELIGIBILITY_GATE: 'FEED_ELIGIBILITY_GATE', -} as const -export type GateType = typeof GateType[keyof typeof GateType] - -export const GATE_LABELS: Record = { - EVIDENCE_GATE: 'Evidenz-Gate', - CONFIDENCE_GATE: 'Konfidenz-Gate', - SENSITIVITY_GATE: 'Sensitivitäts-Gate', - REVIEW_GATE: 'Review-Gate', - MATCHABILITY_GATE: 'Matchbarkeits-Gate', - FEED_ELIGIBILITY_GATE: 'Feed-Eignung', -} - -// ── Gate Status ─────────────────────────────────────────────────────────────── -export const GateStatus = { - PASSED: 'PASSED', - FAILED: 'FAILED', - PENDING: 'PENDING', - BLOCKED: 'BLOCKED', - SKIPPED: 'SKIPPED', -} as const -export type GateStatus = typeof GateStatus[keyof typeof GateStatus] - -export const GATE_STATUS_LABELS: Record = { - PASSED: 'Bestanden', - FAILED: 'Fehlgeschlagen', - PENDING: 'Ausstehend', - BLOCKED: 'Blockiert', - SKIPPED: 'Übersprungen', -} - -export const GATE_STATUS_COLORS: Record = { - PASSED: { bg: 'rgba(22,163,74,0.1)', fg: '#15803d' }, - FAILED: { bg: 'rgba(239,68,68,0.1)', fg: '#dc2626' }, - PENDING: { bg: 'rgba(234,179,8,0.1)', fg: '#a16207' }, - BLOCKED: { bg: 'rgba(239,68,68,0.08)', fg: '#dc2626' }, - SKIPPED: { bg: 'rgba(148,163,184,0.1)', fg: '#64748b' }, -} - -// ── Interfaces ──────────────────────────────────────────────────────────────── -export interface GateCheck { - label: string - passed: boolean - value?: string - note?: string -} - -export interface GateEvaluation { - gateType: GateType - status: GateStatus - reason: string - nextAction?: string - evaluatedAt: string - checks: GateCheck[] -} - -export interface PipelineState { - signalId: string - currentStage: PipelineStage - gates: Record - overallEligible: boolean - publishedToFutureAvailability: boolean - publishedAt?: string - feedDisclaimer?: string -} - -export interface AuditTrailEntry { - id: string - signalId: string - timestamp: string - stage: PipelineStage - action: string - performedBy: string - details: string - gateType?: GateType -} diff --git a/.claude/worktrees/agent-a82a3716/src/domain/unifiedResult.ts b/.claude/worktrees/agent-a82a3716/src/domain/unifiedResult.ts deleted file mode 100644 index 51d86ac..0000000 --- a/.claude/worktrees/agent-a82a3716/src/domain/unifiedResult.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { ResultType } from './enums' -import type { Property } from './property' -import type { FutureSignal } from './futureSignal' -import type { Match } from './match' - -// ── Unified Match Result (discriminated union) ──────────────────────────────── -// Wraps the three result types so UI components can handle all three paths -// without type confusion. Discriminate on `resultType`. - -interface UnifiedResultBase { - matchId: string - needId: string - matchScore: number - resultType: ResultType -} - -export interface VerifiedPortfolioResult extends UnifiedResultBase { - resultType: 'VERIFIED_PORTFOLIO' - property: Property - match: Match -} - -export interface ExternalMarketResult extends UnifiedResultBase { - resultType: 'EXTERNAL_MARKET' - property: Property - match: Match -} - -export interface FutureAvailabilityResult extends UnifiedResultBase { - resultType: 'FUTURE_AVAILABILITY' - signal: FutureSignal - match: Match -} - -export type UnifiedMatchResult = - | VerifiedPortfolioResult - | ExternalMarketResult - | FutureAvailabilityResult diff --git a/.claude/worktrees/agent-a82a3716/src/features/matching/matchCardAdapter.ts b/.claude/worktrees/agent-a82a3716/src/features/matching/matchCardAdapter.ts deleted file mode 100644 index 84d9b31..0000000 --- a/.claude/worktrees/agent-a82a3716/src/features/matching/matchCardAdapter.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { - UnifiedMatchResult, - VerifiedPortfolioResult, - ExternalMarketResult, - FutureAvailabilityResult, -} from '../../domain/unifiedResult' -import type { ScoreFactor } from '../../domain/match' -import type { - MatchCardViewModel, - MatchCardAction, - MatchCardReason, -} from '../../components/match-card/MatchCardViewModel' - -const HARD_CRITERIA = new Set(['area', 'location', 'budget', 'timing']) - -function buildReasons(positiveFactors: ScoreFactor[]): MatchCardReason[] { - const reasons: MatchCardReason[] = [] - - const hardFact = positiveFactors.find(f => HARD_CRITERIA.has(f.criterion)) - const softFact = positiveFactors.find(f => !HARD_CRITERIA.has(f.criterion)) - - if (hardFact) { - reasons.push({ - type: 'HARD_FACT', - label: capitalize(hardFact.criterion), - explanation: hardFact.explanation, - score: hardFact.score, - }) - } - if (softFact) { - reasons.push({ - type: 'SOFT_FACTOR', - label: capitalize(softFact.criterion), - explanation: softFact.explanation, - score: softFact.score, - }) - } - - return reasons -} - -function capitalize(s: string): string { - return s.charAt(0).toUpperCase() + s.slice(1) -} - -export function buildMatchCardViewModel( - result: UnifiedMatchResult, - actions: MatchCardAction[], -): MatchCardViewModel { - const { match, matchScore, resultType } = result - - const isFuture = resultType === 'FUTURE_AVAILABILITY' - const property = !isFuture - ? (result as VerifiedPortfolioResult | ExternalMarketResult).property - : undefined - const signal = isFuture - ? (result as FutureAvailabilityResult).signal - : undefined - - const city = property?.location?.city - const district = property?.location?.district - const locationLabel = city - ? `${city}${district ? `, ${district}` : ''}` - : signal?.locationHint ?? '–' - - const availabilityLabel = - property?.availabilityDate ?? - (signal?.timeHorizonMonths ? `~${signal.timeHorizonMonths} Monate` : undefined) - - const sourceLabel = - property?.sourceLabel ?? - property?.sourceMeta?.sourceLabel ?? - property?.sourceMeta?.sourceType - - const externalUrl = property?.sourceUrl ?? property?.sourceMeta?.sourceUrl - - const reasons = buildReasons(match.positiveFactors) - - const dataQualityScore = - property?.dataQuality?.score ?? signal?.confidenceScore ?? 0.5 - - return { - id: result.matchId, - title: - property?.title ?? - signal?.companyName ?? - signal?.locationHint ?? - '–', - resultType, - assetType: property?.assetType, - matchScore, - confidenceScore: match.confidenceLevel, - dataQualityScore, - locationLabel, - availabilityLabel, - sourceLabel, - externalUrl, - reasons, - tradeoffs: match.tradeoffs ?? [], - risks: match.risks ?? [], - missingData: match.missingData ?? [], - actions, - disclaimer: isFuture - ? (signal?.disclaimer ?? 'Probabilistisches Signal – kein bestätigtes Objekt') - : undefined, - explainabilitySummary: match.explainabilitySummary, - isReviewRequired: match.status === 'PENDING_REVIEW', - isStaleData: false, - } -} diff --git a/.claude/worktrees/agent-a82a3716/src/features/matching/rankingEngine.ts b/.claude/worktrees/agent-a82a3716/src/features/matching/rankingEngine.ts deleted file mode 100644 index cb0f0c0..0000000 --- a/.claude/worktrees/agent-a82a3716/src/features/matching/rankingEngine.ts +++ /dev/null @@ -1,211 +0,0 @@ -import type { Need } from '../../domain/need' -import type { Property } from '../../domain/property' -import type { Match, NextBestAction, ScoreBreakdown } from '../../domain/match' -import { MatchStrength, RiskLevel, ResultType, ConfidenceLevel } from '../../domain/enums' -import type { MatchEngineOutput } from '../../domain/scoring' -import { calculateScore } from './scoreCalculator' - -// ── Strength + Risk Classification ─────────────────────────────────────────── - -export function matchStrengthFromScore(score: number): typeof MatchStrength[keyof typeof MatchStrength] { - if (score >= 78) return MatchStrength.STRONG - if (score >= 52) return MatchStrength.MODERATE - return MatchStrength.WEAK -} - -function riskLevelFromRisks(risks: Match['risks']): typeof RiskLevel[keyof typeof RiskLevel] { - if (!risks || risks.length === 0) return RiskLevel.LOW - const levels = risks.map(r => r.level) - if (levels.includes(RiskLevel.CRITICAL)) return RiskLevel.CRITICAL - if (levels.includes(RiskLevel.HIGH)) return RiskLevel.HIGH - if (levels.includes(RiskLevel.MEDIUM)) return RiskLevel.MEDIUM - return RiskLevel.LOW -} - -function confidenceLabelFromScore(score: number): typeof ConfidenceLevel[keyof typeof ConfidenceLevel] { - if (score >= 0.90) return ConfidenceLevel.VERY_HIGH - if (score >= 0.75) return ConfidenceLevel.HIGH - if (score >= 0.55) return ConfidenceLevel.MEDIUM - if (score >= 0.35) return ConfidenceLevel.LOW - return ConfidenceLevel.VERY_LOW -} - -// ── Next Best Action Generator ──────────────────────────────────────────────── - -export function generateNextBestActions( - output: MatchEngineOutput, - property: Property, - _need: Need, -): NextBestAction[] { - const actions: NextBestAction[] = [] - - if (output.excluded) { - actions.push({ - label: 'Kriterien überprüfen', - description: `Ausschlussgrund: ${output.excludedReason}`, - priority: 'HIGH', - actionType: 'REVIEW', - }) - return actions - } - - const score = output.finalScore - - if (score >= 78) { - actions.push({ - label: 'Shortlist hinzufügen', - description: 'Starkes Match — sofort zur Shortlist hinzufügen', - priority: 'HIGH', - actionType: 'SHORTLIST', - }) - actions.push({ - label: 'Besichtigung anfragen', - description: 'Dieses Objekt zeitnah besichtigen', - priority: 'HIGH', - actionType: 'CONTACT', - }) - } else if (score >= 52) { - actions.push({ - label: 'Details verifizieren', - description: 'Mittleres Match — kritische Datenpunkte direkt bestätigen', - priority: 'MEDIUM', - actionType: 'VERIFY', - }) - actions.push({ - label: 'Mit Alternativen vergleichen', - description: 'Dieses Objekt mit anderen Matches vergleichen', - priority: 'MEDIUM', - actionType: 'COMPARE', - }) - } else { - actions.push({ - label: 'Manuell prüfen', - description: 'Schwaches Match — Eignung manuell beurteilen', - priority: 'LOW', - actionType: 'REVIEW', - }) - } - - if (property.resultType === ResultType.FUTURE_AVAILABILITY) { - actions.push({ - label: 'Frühzeitig vormerken', - description: 'Verfügbarkeit unbestätigt — Signal im Auge behalten', - priority: 'MEDIUM', - actionType: 'SCHEDULE', - }) - } - - if (output.missingData.some(m => m.importance === 'CRITICAL' || m.importance === 'HIGH')) { - actions.push({ - label: 'Fehlende Daten anfordern', - description: 'Objektdaten für vollständige Bewertung vervollständigen', - priority: 'HIGH', - actionType: 'VERIFY', - }) - } - - return actions.slice(0, 4) // cap at 4 actions -} - -// ── Build Full Match Entity ─────────────────────────────────────────────────── - -export function buildFullMatch(need: Need, property: Property): Match { - const output = calculateScore(need, property) - const now = new Date().toISOString() - - const scoreBreakdown: ScoreBreakdown = { - hardMatchScore: output.hardMatchScore, - softFactorScore: output.softFactorScore, - confidenceModifier: output.confidenceModifier, - dataQualityModifier: output.dataQualityModifier, - totalScore: output.finalScore, - } - - const matchScore = output.finalScore - const matchStrength = matchStrengthFromScore(matchScore) - const riskLevel = riskLevelFromRisks(output.risks) - const confidenceLevel = property.confidenceScore - - const summary = buildExplainabilitySummary(output, property, matchScore, matchStrength) - - const uncertaintyIndicators: string[] = [] - if (property.resultType === ResultType.FUTURE_AVAILABILITY) uncertaintyIndicators.push('Zukünftiges Signal — nicht bestätigt') - if (property.confidenceScore < 0.60) uncertaintyIndicators.push(`Niedrige Konfidenz (${Math.round(property.confidenceScore * 100)}%)`) - if ((property.dataQuality?.score ?? 1) < 0.55) uncertaintyIndicators.push('Unvollständige Datenbasis') - if (output.missingData.some(m => m.importance === 'CRITICAL')) uncertaintyIndicators.push('Kritische Daten fehlen') - - return { - id: `match-${need.id}-${property.id}`, - needId: need.id, - propertyId: property.id, - resultId: property.id, - resultType: property.resultType, - - matchScore, - matchStrength, - scoreBreakdown, - confidenceLevel, - confidenceLevelLabel: confidenceLabelFromScore(confidenceLevel), - dataConfidenceScore: property.dataQuality?.score, - - positiveFactors: output.positiveFactors, - negativeFactors: output.negativeFactors, - tradeoffs: output.tradeOffs, - tradeOffs: output.tradeOffs, - risks: output.risks, - missingData: output.missingData, - nextBestActions: output.nextBestActions, - explainabilitySummary: summary, - - riskLevel, - uncertaintyIndicators, - - status: undefined, - createdAt: now, - updatedAt: now, - } -} - -function buildExplainabilitySummary( - output: MatchEngineOutput, - property: Property, - score: number, - strength: string, -): string { - if (output.excluded) { - return `Ausgeschlossen: ${output.excludedReason}` - } - const top = output.positiveFactors[0] - const bottom = output.negativeFactors[0] - const futureNote = property.resultType === ResultType.FUTURE_AVAILABILITY - ? ' (Verfügbarkeit unbestätigt)' - : '' - const positive = top ? ` Stärke: ${top.explanation}.` : '' - const negative = bottom ? ` Schwäche: ${bottom.explanation}.` : '' - return `${strength}-Match mit ${score} Punkten${futureNote}.${positive}${negative}` -} - -// ── Ranking ─────────────────────────────────────────────────────────────────── - -export function rankMatches(matches: Match[]): Match[] { - return [...matches].sort((a, b) => { - // Primary: matchScore descending - if (b.matchScore !== a.matchScore) return b.matchScore - a.matchScore - // Secondary: VERIFIED > EXTERNAL > FUTURE - const typeOrder = { VERIFIED_PORTFOLIO: 0, EXTERNAL_MARKET: 1, FUTURE_AVAILABILITY: 2 } - const aOrder = typeOrder[a.resultType ?? 'EXTERNAL_MARKET'] ?? 1 - const bOrder = typeOrder[b.resultType ?? 'EXTERNAL_MARKET'] ?? 1 - if (aOrder !== bOrder) return aOrder - bOrder - // Tertiary: higher confidence first - return (b.confidenceLevel ?? 0) - (a.confidenceLevel ?? 0) - }) -} - -// ── Batch computation ───────────────────────────────────────────────────────── - -export function computeRankedMatches(need: Need, properties: Property[]): Match[] { - const matches = properties - .map(p => buildFullMatch(need, p)) - .filter(m => !m.matchScore || m.matchScore > 0) // exclude hard-filtered - return rankMatches(matches) -} diff --git a/.claude/worktrees/agent-a82a3716/src/features/matching/scoreCalculator.ts b/.claude/worktrees/agent-a82a3716/src/features/matching/scoreCalculator.ts deleted file mode 100644 index 3699dac..0000000 --- a/.claude/worktrees/agent-a82a3716/src/features/matching/scoreCalculator.ts +++ /dev/null @@ -1,414 +0,0 @@ -import type { Need } from '../../domain/need' -import type { Property } from '../../domain/property' -import type { ScoreFactor } from '../../domain/match' -import { ResultType, AvailabilityStatus, AssetType } from '../../domain/enums' -import { - HARD_FILTER, - DATA_QUALITY_MODIFIER, - CONFIDENCE_MODIFIER, - HARD_CRITERION_KEYS, - SOFT_FACTOR_KEYS, - DEFAULT_SCORING_PROFILES, -} from '../../domain/scoring' -import type { ScoringWeightProfile, HardFilterResult, MatchEngineOutput, SoftFactorKey } from '../../domain/scoring' -import { analyzeTradeOffs, analyzeRisks, identifyMissingData } from './tradeOffAnalyzer' -import { generateNextBestActions } from './rankingEngine' - -// ── Profile resolution ──────────────────────────────────────────────────────── - -function resolveProfile(need: Need, property: Property): ScoringWeightProfile { - const base = { ...(DEFAULT_SCORING_PROFILES[property.assetType] ?? DEFAULT_SCORING_PROFILES.DEFAULT) } - const np = need.weightingProfile - if (!np) return base - - // Apply need's custom core weights, then renormalize the full profile to 1.00 - const CORE = ['area', 'location', 'budget', 'timing', 'prestige', 'accessibility', 'expansionPotential', 'flexibility'] - for (const key of CORE) { - if (typeof np[key] === 'number') base[key] = np[key] - } - const total = Object.values(base).reduce((s, v) => s + v, 0) - if (total > 0) for (const key of Object.keys(base)) base[key] /= total - return base -} - -// ── Hard Filters ────────────────────────────────────────────────────────────── - -export function applyHardFilters(need: Need, property: Property): HardFilterResult { - const assetOk = property.assetType === need.assetType - || property.assetType === AssetType.MIXED - || need.assetType === AssetType.UNKNOWN - - if (!assetOk) { - return { excluded: true, reason: `Nutzungstyp ${property.assetType} stimmt nicht mit ${need.assetType} überein`, severePenalty: 0 } - } - - // Area: hard exclude below tolerance - const areaMin = need.requiredArea?.min ?? 0 - const propArea = property.areaSqmMin ?? property.areaSqm - if (areaMin > 0 && propArea < areaMin * HARD_FILTER.AREA_MIN_TOLERANCE) { - return { - excluded: true, - reason: `Fläche ${propArea} m² unterschreitet Minimum ${areaMin} m² um mehr als ${Math.round((1 - HARD_FILTER.AREA_MIN_TOLERANCE) * 100)}%`, - severePenalty: 0, - } - } - - // Region exclusion - const city = property.location.city.toLowerCase() - const excluded = (need.excludedLocations ?? []).map(l => l.toLowerCase()) - if (excluded.some(e => city.includes(e) || e.includes(city))) { - return { excluded: true, reason: `Standort ${property.location.city} ist ausgeschlossen`, severePenalty: 0 } - } - - // Budget: hard exclude if massively over - const maxBudget = need.budgetRange?.maxPerSqm ?? 0 - if (maxBudget > 0 && property.rentPricePerSqm > maxBudget * HARD_FILTER.BUDGET_EXCLUSION_RATIO) { - return { - excluded: true, - reason: `Miete CHF ${property.rentPricePerSqm}/m² überschreitet Budget CHF ${maxBudget}/m² um mehr als ${Math.round((HARD_FILTER.BUDGET_EXCLUSION_RATIO - 1) * 100)}%`, - severePenalty: 0, - } - } - - // Usage/zoning: occupied property is severe penalty, not exclude - if (property.availabilityStatus === AvailabilityStatus.OCCUPIED) { - return { excluded: false, reason: undefined, severePenalty: 25 } - } - - return { excluded: false, reason: undefined, severePenalty: 0 } -} - -// ── Hard Criterion Scorers ──────────────────────────────────────────────────── - -function scoreArea(need: Need, property: Property, weight: number): ScoreFactor { - const { min = 0, max = Infinity } = need.requiredArea ?? {} - const area = property.areaSqm - const areaMin = property.areaSqmMin ?? area - const areaMax = property.areaSqmMax ?? area - - let score: number - let explanation: string - - // Flexible property — check range overlap - const overlap = areaMin <= max && areaMax >= min - if (overlap) { - score = 100 - explanation = `Fläche ${areaMin === areaMax ? `${area}` : `${areaMin}–${areaMax}`} m² deckt Bedarf ${min}–${max} m² ab` - } else if (area > max) { - const ratio = area / max - score = ratio <= HARD_FILTER.AREA_MAX_RATIO - ? Math.max(40, Math.round(100 - (ratio - 1) * 50)) - : 20 - explanation = `Fläche ${area} m² überschreitet Maximum ${max} m² (${Math.round((ratio - 1) * 100)}% zu viel)` - } else { - // Area between tolerance and min — mild penalty - const ratio = area / min - score = Math.round(40 + ratio * 30) - explanation = `Fläche ${area} m² leicht unter Minimum ${min} m²` - } - - return { criterion: 'area', weight, score, contribution: score * weight, explanation } -} - -function scoreLocation(need: Need, property: Property, weight: number): ScoreFactor { - const city = property.location.city.toLowerCase() - const canton = (property.location.canton ?? '').toLowerCase() - const preferred = (need.preferredLocations ?? []).map(l => l.toLowerCase()) - - let score: number - let explanation: string - - if (preferred.length === 0) { - score = 70 - explanation = 'Kein Standortwunsch — neutral bewertet' - } else if (preferred.some(p => city.includes(p) || p.includes(city))) { - score = 100 - explanation = `Standort ${property.location.city} entspricht Präferenz` - } else if (canton && preferred.some(p => p.includes(canton) || canton.includes(p))) { - score = 60 - explanation = `Gleicher Kanton wie Präferenz (${property.location.canton})` - } else { - score = 35 - explanation = `Standort ${property.location.city} nicht in Präferenzliste` - } - - return { criterion: 'location', weight, score, contribution: score * weight, explanation } -} - -function scoreBudget(need: Need, property: Property, weight: number): ScoreFactor { - const maxBudget = need.budgetRange?.maxPerSqm ?? 0 - const rent = property.rentPricePerSqm - - let score: number - let explanation: string - - if (maxBudget <= 0) { - score = 60 - explanation = 'Kein Budget angegeben — neutral bewertet' - } else if (rent <= maxBudget) { - const ratio = rent / maxBudget - // Very cheap can indicate quality issues — slight penalty below 50% of budget - score = ratio >= 0.50 ? 100 : 88 - explanation = `Miete CHF ${rent}/m² liegt ${Math.round((1 - ratio) * 100)}% unter Budget CHF ${maxBudget}/m²` - } else { - const overRatio = rent / maxBudget - if (overRatio <= HARD_FILTER.BUDGET_MODERATE_RATIO) { - score = 75 - explanation = `Miete CHF ${rent}/m² leicht über Budget (+${Math.round((overRatio - 1) * 100)}%)` - } else if (overRatio <= HARD_FILTER.BUDGET_SEVERE_RATIO) { - score = 45 - explanation = `Miete CHF ${rent}/m² merklich über Budget (+${Math.round((overRatio - 1) * 100)}%)` - } else { - score = 20 - explanation = `Miete CHF ${rent}/m² stark über Budget (+${Math.round((overRatio - 1) * 100)}%)` - } - } - - return { criterion: 'budget', weight, score, contribution: score * weight, explanation } -} - -function scoreTiming(need: Need, property: Property, weight: number): ScoreFactor { - const isFutureSignal = property.resultType === ResultType.FUTURE_AVAILABILITY - - const rawDate = property.availabilityDate - const propDate = rawDate && rawDate !== '' ? new Date(rawDate) : null - const earliest = need.timing?.earliestMoveIn ? new Date(need.timing.earliestMoveIn) : null - const latest = need.timing?.latestMoveIn ? new Date(need.timing.latestMoveIn) : null - const graceMs = HARD_FILTER.TIMING_GRACE_DAYS * 86_400_000 - - let score: number - let explanation: string - - // CRITICAL RULE: Future availability is never treated as confirmed - if (isFutureSignal) { - if (!propDate) { - score = 30 - explanation = 'Zukünftiges Signal — kein Datum, Verfügbarkeit unbestätigt' - } else if (latest && propDate.getTime() > latest.getTime() + graceMs) { - score = 20 - explanation = `Zukünftiges Signal — erwartet ${rawDate}, nach gewünschtem Zeitfenster (unbestätigt)` - } else if (earliest && propDate.getTime() < earliest.getTime()) { - score = 50 - explanation = `Zukünftiges Signal — erwartet ${rawDate}, vor gewünschtem Einzug (unbestätigt)` - } else { - score = 42 - explanation = `Zukünftiges Signal — Zeitfenster passt, Verfügbarkeit jedoch unbestätigt` - } - return { criterion: 'timing', weight, score, contribution: score * weight, explanation } - } - - const isNow = property.availabilityStatus === AvailabilityStatus.AVAILABLE_NOW - || property.availabilityStatus === AvailabilityStatus.AVAILABLE_SOON - - if (isNow) { - const tooEarly = earliest && new Date() < earliest - score = tooEarly ? 80 : 100 - explanation = tooEarly - ? `Sofort verfügbar — Einzug jedoch erst ab ${need.timing?.earliestMoveIn} geplant` - : 'Sofort verfügbar — entspricht Verfügbarkeitswunsch' - return { criterion: 'timing', weight, score, contribution: score * weight, explanation } - } - - if (!propDate) { - score = 38 - explanation = 'Kein Verfügbarkeitsdatum angegeben' - return { criterion: 'timing', weight, score, contribution: score * weight, explanation } - } - - if (earliest && latest) { - const t = propDate.getTime() - if (t >= earliest.getTime() && t <= latest.getTime()) { - score = 95 - explanation = `Verfügbar ${rawDate} liegt im Einzugsfenster` - } else if (t < earliest.getTime()) { - const diff = earliest.getTime() - t - score = diff < graceMs ? 80 : 65 - explanation = `Verfügbar ${rawDate} vor gewünschtem Einzug — kurze Leerstandszeit` - } else { - const diff = t - latest.getTime() - score = diff < graceMs ? 55 : 28 - explanation = `Verfügbar ${rawDate} nach gewünschtem Zeitfenster` - } - } else { - score = 65 - explanation = `Verfügbar ${rawDate}` - } - - return { criterion: 'timing', weight, score, contribution: score * weight, explanation } -} - -// ── Soft Factor Scorer ──────────────────────────────────────────────────────── - -function scoreSoftFactor(key: SoftFactorKey, weight: number, property: Property): ScoreFactor { - const sf = property.softFactors - const hf = property.hardFacts - - const rawValue = (() => { - switch (key) { - case 'prestige': return sf?.prestigeScore ?? sf?.prestige - case 'accessibility': return sf?.commuterAccessScore ?? sf?.accessibility - ?? (hf?.publicTransportScore !== undefined ? hf.publicTransportScore / 10 : undefined) - case 'expansionPotential': return sf?.expansionPotentialScore - case 'flexibility': return sf?.flexibilityScore - case 'visibility': return sf?.visibilityScore - case 'footfall': return sf?.footfallScore - case 'talentAccess': return sf?.talentAccessScore ?? sf?.talentAccess - case 'esg': return sf?.esgScore - case 'taxEnvironment': return sf?.taxEnvironmentScore - default: return undefined - } - })() - - if (rawValue === undefined || rawValue === null) { - // Missing data → neutral 50 (does not help, does not hurt) - return { - criterion: key, - weight, - score: 50, - contribution: 50 * weight, - explanation: `${key}: keine Daten verfügbar — neutral bewertet`, - } - } - - // Soft factor values are 0–1 scale → convert to 0–100 - const score = Math.round(Math.min(100, Math.max(0, rawValue * 100))) - const LABELS: Record = { - prestige: 'Prestige', accessibility: 'Erreichbarkeit', expansionPotential: 'Expansion', - flexibility: 'Flexibilität', visibility: 'Sichtbarkeit', footfall: 'Passantenfrequenz', - talentAccess: 'Talent-Zugang', esg: 'ESG', taxEnvironment: 'Steuerumfeld', - } - return { - criterion: key, - weight, - score, - contribution: score * weight, - explanation: `${LABELS[key] ?? key}: ${score}/100`, - } -} - -// ── Modifier Calculators ────────────────────────────────────────────────────── - -export function calcDataQualityModifier(property: Property): number { - const s = property.dataQuality?.score ?? 0.5 - if (s >= 0.85) return DATA_QUALITY_MODIFIER.EXCELLENT - if (s >= 0.70) return DATA_QUALITY_MODIFIER.GOOD - if (s >= 0.55) return DATA_QUALITY_MODIFIER.FAIR - if (s >= 0.40) return DATA_QUALITY_MODIFIER.POOR - return DATA_QUALITY_MODIFIER.CRITICAL -} - -export function calcConfidenceModifier(property: Property): number { - let mod = 0 - if (property.resultType === ResultType.FUTURE_AVAILABILITY) { - mod += CONFIDENCE_MODIFIER.FUTURE_AVAILABILITY - } else if (property.resultType === ResultType.EXTERNAL_MARKET) { - mod += CONFIDENCE_MODIFIER.EXTERNAL_MARKET - } else { - // VERIFIED_PORTFOLIO - mod += property.confidenceScore >= 0.80 - ? CONFIDENCE_MODIFIER.VERIFIED_HIGH - : CONFIDENCE_MODIFIER.VERIFIED_MEDIUM - } - if (property.confidenceScore < 0.50) { - mod += CONFIDENCE_MODIFIER.LOW_CONFIDENCE - } - return mod -} - -// ── Main Engine Function ────────────────────────────────────────────────────── - -export function calculateScore(need: Need, property: Property): MatchEngineOutput { - const hardFilter = applyHardFilters(need, property) - - if (hardFilter.excluded) { - return { - propertyId: property.id, - needId: need.id, - excluded: true, - excludedReason: hardFilter.reason, - finalScore: 0, - hardMatchScore: 0, - softFactorScore: 0, - dataQualityModifier: 0, - confidenceModifier: 0, - positiveFactors: [], - negativeFactors: [], - allHardFactors: [], - allSoftFactors: [], - tradeOffs: [], - risks: [], - missingData: identifyMissingData(property, need), - nextBestActions: [], - } - } - - const profile = resolveProfile(need, property) - - // ── Hard criteria scoring ────────────────────────────────────────────────── - const hardFactors: ScoreFactor[] = [ - scoreArea(need, property, profile.area), - scoreLocation(need, property, profile.location), - scoreBudget(need, property, profile.budget), - scoreTiming(need, property, profile.timing), - ] - const hardWeightSum = HARD_CRITERION_KEYS.reduce((s, k) => s + profile[k], 0) - const hardRaw = hardFactors.reduce((s, f) => s + f.contribution, 0) - const hardMatchScore = hardWeightSum > 0 ? Math.min(100, Math.round(hardRaw / hardWeightSum)) : 0 - - // ── Soft factor scoring ──────────────────────────────────────────────────── - const softFactors: ScoreFactor[] = SOFT_FACTOR_KEYS - .filter(k => (profile[k] ?? 0) > 0) - .map(k => scoreSoftFactor(k, profile[k], property)) - const softWeightSum = SOFT_FACTOR_KEYS.reduce((s, k) => s + (profile[k] ?? 0), 0) - const softRaw = softFactors.reduce((s, f) => s + f.contribution, 0) - const softFactorScore = softWeightSum > 0 ? Math.min(100, Math.round(softRaw / softWeightSum)) : 50 - - // ── Modifiers ────────────────────────────────────────────────────────────── - const dqMod = calcDataQualityModifier(property) - const confMod = calcConfidenceModifier(property) - - // ── Final score: weighted sum of both groups + modifiers ────────────────── - // Each group already normalized 0–100; combine per SCORE_SPLIT, then apply modifiers - const baseScore = hardMatchScore * 0.60 + softFactorScore * 0.40 - const rawFinal = baseScore + dqMod + confMod - hardFilter.severePenalty - const finalScore = Math.round(Math.min(100, Math.max(0, rawFinal))) - - // ── Factor classification ────────────────────────────────────────────────── - const allFactors = [...hardFactors, ...softFactors] - const THRESHOLD_POSITIVE = 70 - const THRESHOLD_NEGATIVE = 45 - const positiveFactors = allFactors - .filter(f => f.score >= THRESHOLD_POSITIVE) - .sort((a, b) => b.contribution - a.contribution) - .slice(0, 4) - const negativeFactors = allFactors - .filter(f => f.score < THRESHOLD_NEGATIVE) - .sort((a, b) => a.contribution - b.contribution) - .slice(0, 4) - - const tradeOffs = analyzeTradeOffs(hardFactors, softFactors, need, property) - const risks = analyzeRisks(property, hardFactors) - const missingData = identifyMissingData(property, need) - - const output: MatchEngineOutput = { - propertyId: property.id, - needId: need.id, - excluded: false, - finalScore, - hardMatchScore, - softFactorScore, - dataQualityModifier: dqMod, - confidenceModifier: confMod, - positiveFactors, - negativeFactors, - allHardFactors: hardFactors, - allSoftFactors: softFactors, - tradeOffs, - risks, - missingData, - nextBestActions: [], // filled by rankingEngine - } - - output.nextBestActions = generateNextBestActions(output, property, need) - return output -} diff --git a/.claude/worktrees/agent-a82a3716/src/features/matching/tradeOffAnalyzer.ts b/.claude/worktrees/agent-a82a3716/src/features/matching/tradeOffAnalyzer.ts deleted file mode 100644 index fbb9368..0000000 --- a/.claude/worktrees/agent-a82a3716/src/features/matching/tradeOffAnalyzer.ts +++ /dev/null @@ -1,232 +0,0 @@ -import type { Need } from '../../domain/need' -import type { Property } from '../../domain/property' -import type { ScoreFactor, TradeOff, Risk, MissingDataItem } from '../../domain/match' -import { ResultType, AvailabilityStatus } from '../../domain/enums' -import { RiskLevel } from '../../domain/enums' - -// ── Trade-Off Detection ─────────────────────────────────────────────────────── - -export function analyzeTradeOffs( - hardFactors: ScoreFactor[], - softFactors: ScoreFactor[], - need: Need, - property: Property, -): TradeOff[] { - const tradeOffs: TradeOff[] = [] - const byKey = (factors: ScoreFactor[], key: string) => factors.find(f => f.criterion === key) - - const location = byKey(hardFactors, 'location') - const budget = byKey(hardFactors, 'budget') - const area = byKey(hardFactors, 'area') - const timing = byKey(hardFactors, 'timing') - const prestige = byKey(softFactors, 'prestige') - const flex = byKey(softFactors, 'flexibility') - const access = byKey(softFactors, 'accessibility') - - // Prime location at budget premium - if (location && budget && location.score >= 85 && budget.score < 60) { - tradeOffs.push({ - criterion: 'location-vs-budget', - concern: `Erstklassiger Standort (${property.location.city}) zu erhöhten Mietkosten`, - severity: budget.score < 40 ? 'HIGH' : 'MEDIUM', - mitigation: 'Nebenkosten analysieren; längere Laufzeit für Konditionenverhandlung nutzen', - impactOnScore: -Math.round((100 - budget.score) * budget.weight * 10), - }) - } - - // Large space but poor budget fit - if (area && budget && area.score >= 80 && budget.score < 55) { - tradeOffs.push({ - criterion: 'area-vs-budget', - concern: 'Grosszügige Fläche übersteigt Budget — Teiluntermiete denkbar', - severity: 'MEDIUM', - mitigation: 'Möglichkeit für Untermiete oder Co-Working prüfen', - impactOnScore: -5, - }) - } - - // Good timing but low data quality - if (timing && timing.score >= 80 && (property.dataQuality?.score ?? 1) < 0.55) { - tradeOffs.push({ - criterion: 'timing-vs-dataQuality', - concern: 'Verfügbarkeit stimmt, Datenbasis ist aber noch unvollständig', - severity: 'MEDIUM', - mitigation: 'Objektdaten vor Zusage direkt beim Vermieter verifizieren', - impactOnScore: -8, - }) - } - - // High prestige but low flexibility - if (prestige && flex && prestige.score >= 75 && flex.score < 40) { - tradeOffs.push({ - criterion: 'prestige-vs-flexibility', - concern: 'Repräsentative Lage mit eingeschränkter Vertragsflexibilität', - severity: 'LOW', - mitigation: 'Breakclause-Option in Verhandlung einfordern', - impactOnScore: -4, - }) - } - - // Future signal with good location - if (property.resultType === ResultType.FUTURE_AVAILABILITY && location && location.score >= 85) { - tradeOffs.push({ - criterion: 'futureSignal-vs-location', - concern: 'Sehr guter Standort, aber Verfügbarkeit noch unbestätigt', - severity: 'HIGH', - mitigation: 'Frühzeitig Kontakt mit Eigentümer aufnehmen; Letter of Intent erwägen', - impactOnScore: -12, - }) - } - - // Good accessibility but poor public transport - if (access && access.score < 40 && need.softFactors?.maxPublicTransportMinutes !== undefined) { - tradeOffs.push({ - criterion: 'accessibility-vs-commute', - concern: 'Erreichbarkeit unter Ihren Anforderungen — Pendlererfahrung beeinträchtigt', - severity: 'MEDIUM', - mitigation: 'Shuttle-Service oder Mobility-Angebot als Kompensation anfragen', - impactOnScore: -6, - }) - } - - return tradeOffs -} - -// ── Risk Analysis ───────────────────────────────────────────────────────────── - -export function analyzeRisks(property: Property, hardFactors: ScoreFactor[]): Risk[] { - const risks: Risk[] = [] - const byKey = (key: string) => hardFactors.find(f => f.criterion === key) - - // Future availability risk — always flag - if (property.resultType === ResultType.FUTURE_AVAILABILITY) { - risks.push({ - category: 'Verfügbarkeit', - description: 'Zukünftiges Signal — Verfügbarkeit ist nicht bestätigt und kann sich verschieben oder entfallen', - level: RiskLevel.HIGH, - mitigation: 'Absichtserklärung einholen; alternative Objekte parallel prüfen', - }) - } - - // Data quality risk - const dq = property.dataQuality?.score ?? 0.5 - if (dq < 0.55) { - risks.push({ - category: 'Datenqualität', - description: `Datenqualität ${Math.round(dq * 100)}% — Angaben unvollständig oder nicht verifiziert`, - level: dq < 0.40 ? RiskLevel.HIGH : RiskLevel.MEDIUM, - mitigation: 'Objektdaten direkt beim Anbieter anfordern und validieren', - }) - } - - // Budget risk - const budgetFactor = byKey('budget') - if (budgetFactor && budgetFactor.score < 50) { - risks.push({ - category: 'Budget', - description: 'Mietpreis liegt über dem gesetzten Budget — finanzielle Belastung prüfen', - level: budgetFactor.score < 30 ? RiskLevel.HIGH : RiskLevel.MEDIUM, - mitigation: 'Vollkostenrechnung inkl. Nebenkosten erstellen; Verhandlungsspielraum ausloten', - }) - } - - // Occupied / delayed availability - if (property.availabilityStatus === AvailabilityStatus.OCCUPIED) { - risks.push({ - category: 'Verfügbarkeit', - description: 'Objekt aktuell belegt — Übergabetermin unsicher', - level: RiskLevel.MEDIUM, - mitigation: 'Verbindlichen Übergabetermin schriftlich vereinbaren', - }) - } - - // Low confidence score - if (property.confidenceScore < 0.50) { - risks.push({ - category: 'Datenverlässlichkeit', - description: `Konfidenz ${Math.round(property.confidenceScore * 100)}% — Quelldaten unsicher`, - level: RiskLevel.MEDIUM, - mitigation: 'Unabhängige Verifikation der Objektangaben empfohlen', - }) - } - - // Missing critical property data - const criticalMissing = property.dataQuality?.missingCriticalFields ?? [] - if (criticalMissing.length > 0) { - risks.push({ - category: 'Fehlende Kerndaten', - description: `Fehlende Pflichtfelder: ${criticalMissing.slice(0, 3).join(', ')}${criticalMissing.length > 3 ? ` +${criticalMissing.length - 3}` : ''}`, - level: RiskLevel.MEDIUM, - mitigation: 'Objektdaten vor Verhandlung vervollständigen lassen', - }) - } - - return risks -} - -// ── Missing Data Detection ──────────────────────────────────────────────────── - -export function identifyMissingData(property: Property, need: Need): MissingDataItem[] { - const missing: MissingDataItem[] = [] - - if (!property.rentPricePerSqm || property.rentPricePerSqm <= 0) { - missing.push({ - field: 'rentPricePerSqm', - importance: 'CRITICAL', - description: 'Mietpreis fehlt — Budget-Scoring nicht möglich', - impact: 'Budget-Score wird neutral (50) gesetzt — Gesamtscore unzuverlässig', - }) - } - - if (!property.availabilityDate || property.availabilityDate === '') { - missing.push({ - field: 'availabilityDate', - importance: 'HIGH', - description: 'Kein Verfügbarkeitsdatum angegeben', - impact: 'Timing-Score reduziert auf 38/100 — Einzugsfenster nicht prüfbar', - }) - } - - if (!property.softFactors) { - missing.push({ - field: 'softFactors', - importance: 'HIGH', - description: 'Soft Factors vollständig fehlend (Prestige, Erreichbarkeit, etc.)', - impact: 'Alle Soft-Factor-Scores auf neutral (50) gesetzt — Matching-Qualität eingeschränkt', - }) - } else { - const sf = property.softFactors - const missingFields: Array<[string, string]> = [] - if (sf.commuterAccessScore === undefined && sf.accessibility === undefined) missingFields.push(['accessibility', 'Erreichbarkeit']) - if (sf.prestigeScore === undefined && sf.prestige === undefined) missingFields.push(['prestige', 'Prestige-Score']) - if (sf.esgScore === undefined) missingFields.push(['esgScore', 'ESG-Bewertung']) - if (missingFields.length > 0) { - missing.push({ - field: missingFields.map(([k]) => k).join(', '), - importance: 'MEDIUM', - description: `Fehlende Soft Factors: ${missingFields.map(([, l]) => l).join(', ')}`, - impact: 'Betroffene Scores neutral — Matching-Präzision verringert', - }) - } - } - - if (!property.hardFacts) { - missing.push({ - field: 'hardFacts', - importance: 'MEDIUM', - description: 'Technische Objektdaten fehlen (Parkierung, ÖV-Score, etc.)', - impact: 'Infrastruktureignung nicht prüfbar', - }) - } - - if (need.budgetRange?.maxPerSqm === undefined || need.budgetRange.maxPerSqm <= 0) { - missing.push({ - field: 'need.budgetRange', - importance: 'HIGH', - description: 'Kein Budget im Bedarf angegeben', - impact: 'Budget-Scoring neutralisiert — Filter unwirksam', - }) - } - - return missing -} diff --git a/.claude/worktrees/agent-a82a3716/src/hooks/index.ts b/.claude/worktrees/agent-a82a3716/src/hooks/index.ts deleted file mode 100644 index 4d9e3ef..0000000 --- a/.claude/worktrees/agent-a82a3716/src/hooks/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { useProperties, useProperty, usePropertyDetail } from './useProperties' -export { useMatches, useMatchesByNeed, useMatchesByProperty, useApproveMatch, useMatchDetail } from './useMatches' -export { useFutureSignals, useFutureSignalsByProperty, useVerifySignal } from './useFutureSignals' -export { useNeeds, useNeed, useNeedProfiles } from './useNeeds' -export { useUnifiedResults } from './useUnifiedResults' -export { useReviewQueue, useApproveReviewItem, useRejectReviewItem } from './useReviewQueue' diff --git a/.claude/worktrees/agent-a82a3716/src/hooks/useAIMonitoring.ts b/.claude/worktrees/agent-a82a3716/src/hooks/useAIMonitoring.ts deleted file mode 100644 index 48b7555..0000000 --- a/.claude/worktrees/agent-a82a3716/src/hooks/useAIMonitoring.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { aiMonitoringService } from '../services/aiMonitoringService' -import type { AIMonitoringFilters } from '../provider/IAIMonitoringProvider' -import type { ReviewStatus } from '../domain/enums' - -const QK = 'aiOutputs' -const STALE = 30_000 - -export function useAIOutputs(filters?: AIMonitoringFilters) { - return useQuery({ - queryKey: [QK, filters ?? {}], - queryFn: () => aiMonitoringService.getOutputs(filters), - staleTime: STALE, - select: (res) => res.data ?? [], - }) -} - -export function useAIOutput(id: string | null) { - return useQuery({ - queryKey: [QK, 'detail', id], - queryFn: () => aiMonitoringService.getOutput(id!), - staleTime: STALE, - enabled: !!id, - select: (res) => res.data ?? null, - }) -} - -export function useUpdateAIOutputReviewStatus() { - const qc = useQueryClient() - return useMutation({ - mutationFn: ({ id, status }: { id: string; status: ReviewStatus }) => - aiMonitoringService.updateReviewStatus(id, status), - onSuccess: () => { - qc.invalidateQueries({ queryKey: [QK] }) - }, - }) -} diff --git a/.claude/worktrees/agent-a82a3716/src/hooks/useDataSources.ts b/.claude/worktrees/agent-a82a3716/src/hooks/useDataSources.ts deleted file mode 100644 index e3d3420..0000000 --- a/.claude/worktrees/agent-a82a3716/src/hooks/useDataSources.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { sourceService } from '../services/sourceService' -import type { SourceFilters, SourceStatus, TermsStatus } from '../domain/dataSource' - -const STALE_SOURCES = 30_000 - -export function useDataSources(filters?: SourceFilters) { - return useQuery({ - queryKey: ['data-sources', filters ?? {}], - queryFn: () => sourceService.getSources(filters), - staleTime: STALE_SOURCES, - select: (res) => res.data ?? [], - }) -} - -export function useDataSource(id: string | null) { - return useQuery({ - queryKey: ['data-source', id], - queryFn: () => sourceService.getSource(id!), - enabled: id !== null, - staleTime: STALE_SOURCES, - select: (res) => res.data ?? null, - }) -} - -export function useConnectorRuns(sourceId: string | null) { - return useQuery({ - queryKey: ['connector-runs', sourceId], - queryFn: () => sourceService.getConnectorRuns(sourceId!), - enabled: sourceId !== null, - staleTime: STALE_SOURCES, - select: (res) => res.data ?? [], - }) -} - -export function useTriggerMockRun() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: (sourceId: string) => sourceService.triggerMockRun(sourceId), - onSuccess: (_data, sourceId) => { - queryClient.invalidateQueries({ queryKey: ['data-sources'] }) - queryClient.invalidateQueries({ queryKey: ['data-source', sourceId] }) - queryClient.invalidateQueries({ queryKey: ['connector-runs', sourceId] }) - }, - }) -} - -export function useUpdateSourceStatus() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ id, status }: { id: string; status: SourceStatus }) => - sourceService.updateSourceStatus(id, status), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['data-sources'] }) - queryClient.invalidateQueries({ queryKey: ['data-source'] }) - }, - }) -} - -export function useMarkTermsStatus() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ id, termsStatus }: { id: string; termsStatus: TermsStatus }) => - sourceService.markTermsStatus(id, termsStatus), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['data-sources'] }) - queryClient.invalidateQueries({ queryKey: ['data-source'] }) - }, - }) -} diff --git a/.claude/worktrees/agent-a82a3716/src/hooks/useFutureSignals.ts b/.claude/worktrees/agent-a82a3716/src/hooks/useFutureSignals.ts deleted file mode 100644 index ceec3d2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/hooks/useFutureSignals.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { futureSignalService } from '../services/futureSignalService' -import type { ReviewStatus } from '../domain/enums' -import { STALE_SIGNALS } from '../lib/constants' - -export function useFutureSignals() { - return useQuery({ - queryKey: ['futureSignals'], - queryFn: () => futureSignalService.getAll(), - staleTime: STALE_SIGNALS, - select: (res) => res.data ?? [], - }) -} - -export function useFutureSignal(id: string) { - return useQuery({ - queryKey: ['futureSignal', id], - queryFn: () => futureSignalService.getById(id), - staleTime: STALE_SIGNALS, - enabled: !!id, - select: (res) => res.data ?? null, - }) -} - -export function useFutureSignalsByProperty(propertyId: string) { - return useQuery({ - queryKey: ['futureSignals', 'property', propertyId], - queryFn: () => futureSignalService.getByProperty(propertyId), - staleTime: STALE_SIGNALS, - enabled: Boolean(propertyId), - select: (res) => res.data ?? [], - }) -} - -export function useVerifySignal() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: (signalId: string) => futureSignalService.verify(signalId, 'admin@ideal-sharing.ch'), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['futureSignals'] }) - }, - }) -} - -export function useUpdateSignalReviewStatus() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ id, status }: { id: string; status: ReviewStatus }) => - futureSignalService.updateReviewStatus(id, status), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['futureSignals'] }) - queryClient.invalidateQueries({ queryKey: ['futureSignal'] }) - }, - }) -} diff --git a/.claude/worktrees/agent-a82a3716/src/hooks/useMarketSignals.ts b/.claude/worktrees/agent-a82a3716/src/hooks/useMarketSignals.ts deleted file mode 100644 index 7e033a1..0000000 --- a/.claude/worktrees/agent-a82a3716/src/hooks/useMarketSignals.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { marketIntelligenceService } from '../services/marketIntelligenceService' -import { reviewService } from '../services/reviewService' -import type { MarketSignalFilters, SignalProcessingStatus } from '../domain/marketSignal' - -const STALE_SIGNALS = 30_000 - -export function useMarketSignals(filters?: MarketSignalFilters) { - return useQuery({ - queryKey: ['market-signals', filters ?? {}], - queryFn: () => marketIntelligenceService.getSignals(filters), - staleTime: STALE_SIGNALS, - select: (res) => res.data ?? [], - }) -} - -export function useMarketSignalDetail(id: string | null) { - return useQuery({ - queryKey: ['market-signal', id], - queryFn: () => marketIntelligenceService.getSignalDetail(id!), - enabled: id !== null, - staleTime: STALE_SIGNALS, - select: (res) => res.data ?? null, - }) -} - -export function useUpdateSignalStatus() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ id, status }: { id: string; status: SignalProcessingStatus }) => - marketIntelligenceService.updateSignalStatus(id, status), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['market-signals'] }) - queryClient.invalidateQueries({ queryKey: ['market-signal'] }) - }, - }) -} - -export function useConvertToFutureSignal() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: (id: string) => marketIntelligenceService.convertToFutureSignal(id), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['market-signals'] }) - queryClient.invalidateQueries({ queryKey: ['market-signal'] }) - }, - }) -} - -export function useLinkSignalToEntity() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ - id, - entityType, - entityId, - }: { - id: string - entityType: 'property' | 'need' - entityId: string - }) => marketIntelligenceService.linkSignalToEntity(id, entityType, entityId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['market-signals'] }) - queryClient.invalidateQueries({ queryKey: ['market-signal'] }) - }, - }) -} - -export function useCreateReviewTask() { - return useMutation({ - mutationFn: (signalId: string) => reviewService.createReviewTask(signalId), - }) -} diff --git a/.claude/worktrees/agent-a82a3716/src/hooks/useMatches.ts b/.claude/worktrees/agent-a82a3716/src/hooks/useMatches.ts deleted file mode 100644 index 7212be7..0000000 --- a/.claude/worktrees/agent-a82a3716/src/hooks/useMatches.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { matchService } from '../services/matchService' -import { STALE_MATCHES } from '../lib/constants' - -export function useMatches() { - return useQuery({ - queryKey: ['matches'], - queryFn: () => matchService.getAll(), - staleTime: STALE_MATCHES, - select: (res) => res.data ?? [], - }) -} - -export function useMatchesByNeed(needId: string) { - return useQuery({ - queryKey: ['matches', 'need', needId], - queryFn: () => matchService.getByNeed(needId), - staleTime: STALE_MATCHES, - enabled: Boolean(needId), - select: (res) => res.data ?? [], - }) -} - -export function useMatchesByProperty(propertyId: string) { - return useQuery({ - queryKey: ['matches', 'property', propertyId], - queryFn: () => matchService.getByProperty(propertyId), - staleTime: STALE_MATCHES, - enabled: Boolean(propertyId), - select: (res) => res.data ?? [], - }) -} - -export function useApproveMatch() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: (matchId: string) => matchService.approve(matchId, 'current-user'), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['matches'] }) - }, - }) -} - -export function useMatchDetail(id: string) { - return useQuery({ - queryKey: ['match', id], - queryFn: () => matchService.getById(id), - staleTime: STALE_MATCHES, - enabled: Boolean(id), - select: (res) => res.data ?? null, - }) -} diff --git a/.claude/worktrees/agent-a82a3716/src/hooks/useNeeds.ts b/.claude/worktrees/agent-a82a3716/src/hooks/useNeeds.ts deleted file mode 100644 index b753daa..0000000 --- a/.claude/worktrees/agent-a82a3716/src/hooks/useNeeds.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { needService } from '../services/needService' - -export function useNeeds() { - return useQuery({ - queryKey: ['needs'], - queryFn: () => needService.getAll(), - select: (res) => res.data ?? [], - }) -} - -export function useNeed(id: string) { - return useQuery({ - queryKey: ['need', id], - queryFn: () => needService.getById(id), - enabled: Boolean(id), - select: (res) => res.data ?? null, - }) -} - -export const useNeedProfiles = useNeeds diff --git a/.claude/worktrees/agent-a82a3716/src/hooks/useProperties.ts b/.claude/worktrees/agent-a82a3716/src/hooks/useProperties.ts deleted file mode 100644 index 9ede0b7..0000000 --- a/.claude/worktrees/agent-a82a3716/src/hooks/useProperties.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { propertyService } from '../services/propertyService' -import { matchService } from '../services/matchService' -import { futureSignalService } from '../services/futureSignalService' -import type { AssetType, ResultType } from '../domain/enums' -import { STALE_PROPERTIES, STALE_MATCHES, STALE_SIGNALS } from '../lib/constants' - -interface PropertyFilter { - assetType?: AssetType - resultType?: ResultType - city?: string - minAreaSqm?: number - maxRentPerSqm?: number - organizationId?: string -} - -export function useProperties(filter?: PropertyFilter) { - return useQuery({ - queryKey: ['properties', filter ?? {}], - queryFn: () => propertyService.getAll(filter), - staleTime: STALE_PROPERTIES, - select: (res) => res.data ?? [], - }) -} - -export function useProperty(id: string) { - return useQuery({ - queryKey: ['property', id], - queryFn: () => propertyService.getById(id), - staleTime: STALE_PROPERTIES, - enabled: Boolean(id), - select: (res) => res.data ?? null, - }) -} - -export const usePropertyDetail = useProperty - -export function usePropertyById(id: string | null) { - return useQuery({ - queryKey: ['property', id], - queryFn: () => propertyService.getById(id!), - enabled: !!id, - staleTime: STALE_PROPERTIES, - select: (res) => res.data ?? null, - }) -} - -export function usePropertyMatches(propertyId: string | null) { - return useQuery({ - queryKey: ['property-matches', propertyId], - queryFn: () => matchService.getMatchesForProperty(propertyId!), - enabled: !!propertyId, - staleTime: STALE_MATCHES, - select: (res) => res.data ?? [], - }) -} - -export function usePropertySignals(propertyId: string | null) { - return useQuery({ - queryKey: ['property-signals', propertyId], - queryFn: () => futureSignalService.getSignalsForProperty(propertyId!), - enabled: !!propertyId, - staleTime: STALE_SIGNALS, - select: (res) => res.data ?? [], - }) -} diff --git a/.claude/worktrees/agent-a82a3716/src/hooks/useReviewQueue.ts b/.claude/worktrees/agent-a82a3716/src/hooks/useReviewQueue.ts deleted file mode 100644 index 282563b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/hooks/useReviewQueue.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { reviewService } from '../services/reviewService' -import { useSessionStore } from '../stores/sessionStore' -import type { ReviewFilters } from '../provider/IReviewProvider' -import type { ReviewTaskStatus } from '../domain/review' - -const STALE_REVIEW = 30_000 -const QK = 'reviewQueue' - -export function useReviewQueue(filters?: ReviewFilters) { - return useQuery({ - queryKey: [QK, filters ?? {}], - queryFn: () => reviewService.getTasks(filters), - staleTime: STALE_REVIEW, - select: (res) => res.data ?? [], - }) -} - -export function useReviewTask(id: string | null) { - return useQuery({ - queryKey: [QK, 'task', id], - queryFn: () => reviewService.getTask(id!), - staleTime: STALE_REVIEW, - enabled: !!id, - select: (res) => res.data ?? null, - }) -} - -export function useUpdateReviewStatus() { - const queryClient = useQueryClient() - const { currentUser } = useSessionStore.getState() - const userId = currentUser?.email ?? 'unknown' - - return useMutation({ - mutationFn: ({ id, status, note }: { id: string; status: ReviewTaskStatus; note?: string }) => - reviewService.updateStatus(id, status, userId, note), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [QK] }) - }, - }) -} - -export function useAssignReviewTask() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ id, assignTo }: { id: string; assignTo: string }) => - reviewService.assign(id, assignTo), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [QK] }) - }, - }) -} - -export function useAddReviewNote() { - const queryClient = useQueryClient() - const { currentUser } = useSessionStore.getState() - const userId = currentUser?.email ?? 'unknown' - - return useMutation({ - mutationFn: ({ id, content }: { id: string; content: string }) => - reviewService.addNote(id, { content, createdBy: userId, createdAt: new Date().toISOString() }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [QK] }) - }, - }) -} - -// Legacy exports -export function useApproveReviewItem() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ id, notes }: { id: string; notes?: string }) => - reviewService.approve(id, 'current-user', notes), - onSuccess: () => queryClient.invalidateQueries({ queryKey: [QK] }), - }) -} - -export function useRejectReviewItem() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ id, notes }: { id: string; notes?: string }) => - reviewService.reject(id, 'current-user', notes), - onSuccess: () => queryClient.invalidateQueries({ queryKey: [QK] }), - }) -} diff --git a/.claude/worktrees/agent-a82a3716/src/hooks/useShortlists.ts b/.claude/worktrees/agent-a82a3716/src/hooks/useShortlists.ts deleted file mode 100644 index 8a21b2a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/hooks/useShortlists.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { shortlistService } from '../services/shortlistService' -import type { CreateShortlistInput, UpdateShortlistInput, ShortlistItemInput } from '../domain/shortlist' - -export function useShortlists() { - return useQuery({ - queryKey: ['shortlists'], - queryFn: () => shortlistService.getAll(), - select: (res) => res.data ?? [], - }) -} - -export function useShortlist(id: string) { - return useQuery({ - queryKey: ['shortlist', id], - queryFn: () => shortlistService.getById(id), - enabled: !!id, - select: (res) => res.data ?? null, - }) -} - -export function useCreateShortlist() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: (input: CreateShortlistInput) => shortlistService.create(input), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['shortlists'] }) - }, - }) -} - -export function useAddToShortlist() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ shortlistId, item }: { shortlistId: string; item: ShortlistItemInput }) => - shortlistService.addItem(shortlistId, item), - onSuccess: (_data, { shortlistId }) => { - queryClient.invalidateQueries({ queryKey: ['shortlists'] }) - queryClient.invalidateQueries({ queryKey: ['shortlist', shortlistId] }) - }, - }) -} - -export function useRemoveFromShortlist() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ shortlistId, resultId }: { shortlistId: string; resultId: string }) => - shortlistService.removeItem(shortlistId, resultId), - onSuccess: (_data, { shortlistId }) => { - queryClient.invalidateQueries({ queryKey: ['shortlists'] }) - queryClient.invalidateQueries({ queryKey: ['shortlist', shortlistId] }) - }, - }) -} - -export function useUpdateShortlist() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ id, data }: { id: string; data: UpdateShortlistInput }) => - shortlistService.update(id, data), - onSuccess: (_data, { id }) => { - queryClient.invalidateQueries({ queryKey: ['shortlists'] }) - queryClient.invalidateQueries({ queryKey: ['shortlist', id] }) - }, - }) -} diff --git a/.claude/worktrees/agent-a82a3716/src/hooks/useSignalPipeline.ts b/.claude/worktrees/agent-a82a3716/src/hooks/useSignalPipeline.ts deleted file mode 100644 index 51ce976..0000000 --- a/.claude/worktrees/agent-a82a3716/src/hooks/useSignalPipeline.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { signalPipelineService } from '../services/signalPipelineService' -import type { GateType } from '../domain/signalPipeline' - -const STALE_PIPELINE = 15_000 - -export function useSignalPipelineState(signalId: string | null) { - return useQuery({ - queryKey: ['signal-pipeline', signalId], - queryFn: () => signalPipelineService.getPipelineState(signalId!), - enabled: signalId !== null, - staleTime: STALE_PIPELINE, - select: (res) => res.data ?? null, - }) -} - -export function useSignalAuditTrail(signalId: string | null) { - return useQuery({ - queryKey: ['signal-audit-trail', signalId], - queryFn: () => signalPipelineService.getAuditTrail(signalId!), - enabled: signalId !== null, - staleTime: STALE_PIPELINE, - select: (res) => res.data ?? [], - }) -} - -export function useEvaluateGate() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: ({ signalId, gateType }: { signalId: string; gateType: GateType }) => - signalPipelineService.evaluateGate(signalId, gateType), - onSuccess: (_data, { signalId }) => { - queryClient.invalidateQueries({ queryKey: ['signal-pipeline', signalId] }) - }, - }) -} - -export function usePublishToFutureAvailability() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: (signalId: string) => signalPipelineService.publishToFutureAvailability(signalId), - onSuccess: (_data, signalId) => { - queryClient.invalidateQueries({ queryKey: ['signal-pipeline', signalId] }) - queryClient.invalidateQueries({ queryKey: ['signal-audit-trail', signalId] }) - queryClient.invalidateQueries({ queryKey: ['market-signals'] }) - }, - }) -} diff --git a/.claude/worktrees/agent-a82a3716/src/hooks/useSupplyDashboard.ts b/.claude/worktrees/agent-a82a3716/src/hooks/useSupplyDashboard.ts deleted file mode 100644 index 9d10620..0000000 --- a/.claude/worktrees/agent-a82a3716/src/hooks/useSupplyDashboard.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { dashboardService } from '../services/dashboardService' -import type { DashboardData } from '../domain/dashboard' - -export function useSupplyDashboard() { - return useQuery({ - queryKey: ['supply', 'dashboard'], - queryFn: () => dashboardService.getDashboardData(), - staleTime: 30_000, - }) -} diff --git a/.claude/worktrees/agent-a82a3716/src/hooks/useUnifiedResults.ts b/.claude/worktrees/agent-a82a3716/src/hooks/useUnifiedResults.ts deleted file mode 100644 index b358e7b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/hooks/useUnifiedResults.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { useMemo } from 'react' -import { useMatches, useMatchesByNeed } from './useMatches' -import { useProperties } from './useProperties' -import { useFutureSignals } from './useFutureSignals' -import type { - UnifiedMatchResult, - VerifiedPortfolioResult, - ExternalMarketResult, - FutureAvailabilityResult, -} from '../domain/unifiedResult' - -export function useUnifiedResults(needId?: string) { - const allMatchesQuery = useMatches() - const needMatchesQuery = useMatchesByNeed(needId ?? '') - const propertiesQuery = useProperties() - const signalsQuery = useFutureSignals() - - const matchesQuery = needId ? needMatchesQuery : allMatchesQuery - - const isLoading = - matchesQuery.isLoading || propertiesQuery.isLoading || signalsQuery.isLoading - const error = matchesQuery.error ?? propertiesQuery.error ?? signalsQuery.error - - const matches = matchesQuery.data ?? [] - const properties = propertiesQuery.data ?? [] - const signals = signalsQuery.data ?? [] - - const data = useMemo((): UnifiedMatchResult[] => { - return matches - .flatMap((match): UnifiedMatchResult[] => { - const rt = match.resultType ?? 'VERIFIED_PORTFOLIO' - - if (rt === 'FUTURE_AVAILABILITY') { - const refId = match.resultId ?? match.propertyId - const signal = signals.find( - s => s.id === refId || s.propertyId === refId, - ) - if (!signal) return [] - const result: FutureAvailabilityResult = { - matchId: match.id, - needId: match.needId, - matchScore: match.matchScore, - resultType: 'FUTURE_AVAILABILITY', - signal, - match, - } - return [result] - } - - const property = properties.find(p => p.id === (match.resultId ?? match.propertyId)) - if (!property) return [] - - if (rt === 'EXTERNAL_MARKET') { - const result: ExternalMarketResult = { - matchId: match.id, - needId: match.needId, - matchScore: match.matchScore, - resultType: 'EXTERNAL_MARKET', - property, - match, - } - return [result] - } - - const result: VerifiedPortfolioResult = { - matchId: match.id, - needId: match.needId, - matchScore: match.matchScore, - resultType: 'VERIFIED_PORTFOLIO', - property, - match, - } - return [result] - }) - .sort((a, b) => b.matchScore - a.matchScore) - }, [matches, properties, signals]) - - return { data, isLoading, error } -} diff --git a/.claude/worktrees/agent-a82a3716/src/index.css b/.claude/worktrees/agent-a82a3716/src/index.css deleted file mode 100644 index a5002d3..0000000 --- a/.claude/worktrees/agent-a82a3716/src/index.css +++ /dev/null @@ -1,4 +0,0 @@ -@layer theme, base, components, utilities; - -@import "tailwindcss/theme.css" layer(theme); -@import "tailwindcss/utilities.css" layer(utilities); diff --git a/.claude/worktrees/agent-a82a3716/src/lib/constants.ts b/.claude/worktrees/agent-a82a3716/src/lib/constants.ts deleted file mode 100644 index 7aa7f0b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/lib/constants.ts +++ /dev/null @@ -1,130 +0,0 @@ -// Centralized app-wide constants — no magic strings anywhere else - -export const APP_NAME = 'Property-Match' -export const APP_VERSION = '0.1.0' - -// Organisation -export const DEFAULT_ORG_ID = 'org-wincasa' - -// Pagination -export const DEFAULT_PAGE_SIZE = 25 -export const MAX_COMPARE_ITEMS = 3 - -// Data quality thresholds -export const DQ_HIGH = 0.8 -export const DQ_MEDIUM = 0.6 - -// Confidence thresholds -export const CONF_HIGH = 0.85 -export const CONF_MEDIUM = 0.65 - -// Match score thresholds (0–100) -export const SCORE_STRONG = 80 -export const SCORE_MODERATE = 60 - -// Probability thresholds (future signals) -export const PROB_HIGH = 0.7 -export const PROB_MEDIUM = 0.5 - -// Query stale times (ms) -export const STALE_PROPERTIES = 5 * 60 * 1000 -export const STALE_MATCHES = 2 * 60 * 1000 -export const STALE_SIGNALS = 5 * 60 * 1000 - -// Route paths — single source of truth -export const ROUTES = { - HOME: '/', - SUPPLY: { - DASHBOARD: '/supply/dashboard', - PROPERTIES: '/supply/properties', - MATCH_CENTER: '/supply/match-center', - FUTURE_AVAILABILITY: '/supply/future-availability', - DATA_QUALITY: '/supply/data-quality', - }, - DEMAND: { - AI_SEARCH: '/demand/ai-search', - RESULTS: '/demand/results', - COMPARE: '/demand/compare', - SHORTLISTS: '/demand/shortlists', - }, - OPS: { - REVIEW_QUEUE: '/ops/review-queue', - AI_MONITORING: '/ops/ai-monitoring', - GOVERNANCE: '/ops/governance', - }, -} as const - -// Asset type display labels -export const ASSET_TYPE_LABELS: Record = { - OFFICE: 'Büro', - RETAIL: 'Retail', - GASTRO: 'Gastronomie', - LOGISTICS: 'Logistik', - PRODUCTION: 'Produktion', - MIXED: 'Gemischt', -} - -// Result type display labels -export const RESULT_TYPE_LABELS: Record = { - VERIFIED_PORTFOLIO: 'Verified Portfolio', - EXTERNAL_MARKET: 'Marktinserat', - FUTURE_AVAILABILITY: 'Zukunftssignal', -} - -// Match strength display labels -export const MATCH_STRENGTH_LABELS: Record = { - STRONG: 'Stark', - MODERATE: 'Mittel', - WEAK: 'Schwach', -} - -// Availability status display labels -export const AVAILABILITY_LABELS: Record = { - AVAILABLE_NOW: 'Verfügbar', - AVAILABLE_SOON: 'Bald verfügbar', - FUTURE_SIGNAL: 'Zukunftssignal', - OCCUPIED: 'Belegt', - UNKNOWN: 'Unbekannt', -} - -// Risk level display labels -export const RISK_LABELS: Record = { - LOW: 'Niedrig', - MEDIUM: 'Mittel', - HIGH: 'Hoch', - CRITICAL: 'Kritisch', -} - -// Signal type display labels -export const SIGNAL_TYPE_LABELS: Record = { - EXPANSION: 'Expansion', - POSSIBLE_MOVE_OUT: 'Möglicher Auszug', - CONSTRUCTION_PROJECT: 'Bauprojekt', - RESTRUCTURING: 'Umstrukturierung', - PROJECT_DEVELOPMENT: 'Projektentwicklung', - SPACE_CONSOLIDATION: 'Flächenkonsolidierung', -} - -// Data freshness display labels -export const FRESHNESS_LABELS: Record = { - FRESH: 'Aktuell', - STALE: 'Veraltet', - OUTDATED: 'Abgelaufen', -} - -// Confidence level display labels -export const CONFIDENCE_LABELS: Record = { - VERY_HIGH: 'Sehr hoch', - HIGH: 'Hoch', - MEDIUM: 'Mittel', - LOW: 'Niedrig', - VERY_LOW: 'Sehr niedrig', -} - -// Data quality level display labels -export const DATA_QUALITY_LABELS: Record = { - HIGH: 'Hoch', - MEDIUM: 'Mittel', - LOW: 'Niedrig', - INCOMPLETE: 'Unvollständig', -} diff --git a/.claude/worktrees/agent-a82a3716/src/lib/ds.ts b/.claude/worktrees/agent-a82a3716/src/lib/ds.ts deleted file mode 100644 index 5e4c2af..0000000 --- a/.claude/worktrees/agent-a82a3716/src/lib/ds.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { ConfidenceLevel, DataQualityLevel } from '../domain/enums' -import { CONF_HIGH, CONF_MEDIUM, DQ_HIGH, DQ_MEDIUM } from './constants' - -// ── Semantic color tokens ───────────────────────────────────────────────────── -// Single source of truth for badge/score colors. All badge components read from here. - -export const DS_COLORS = { - resultType: { - VERIFIED_PORTFOLIO: { bg: 'rgba(30,58,95,0.10)', fg: '#1e3a5f' }, - EXTERNAL_MARKET: { bg: 'rgba(180,83,9,0.10)', fg: '#b45309' }, - FUTURE_AVAILABILITY: { bg: 'rgba(109,40,217,0.10)', fg: '#6d28d9' }, - }, - confidence: { - VERY_HIGH: { bg: 'rgba(26,122,74,0.12)', fg: '#1a7a4a' }, - HIGH: { bg: 'rgba(26,122,74,0.09)', fg: '#1a7a4a' }, - MEDIUM: { bg: 'rgba(30,58,95,0.10)', fg: '#1e3a5f' }, - LOW: { bg: 'rgba(217,119,6,0.12)', fg: '#d97706' }, - VERY_LOW: { bg: 'rgba(192,57,43,0.12)', fg: '#c0392b' }, - }, - risk: { - LOW: { bg: 'rgba(26,122,74,0.10)', fg: '#1a7a4a' }, - MEDIUM: { bg: 'rgba(217,119,6,0.10)', fg: '#d97706' }, - HIGH: { bg: 'rgba(192,57,43,0.10)', fg: '#c0392b' }, - CRITICAL: { bg: 'rgba(127,0,0,0.12)', fg: '#7f1d1d' }, - }, - availability: { - AVAILABLE_NOW: { bg: 'rgba(26,122,74,0.10)', fg: '#1a7a4a' }, - AVAILABLE_SOON: { bg: 'rgba(217,119,6,0.10)', fg: '#d97706' }, - FUTURE_SIGNAL: { bg: 'rgba(109,40,217,0.10)', fg: '#6d28d9' }, - OCCUPIED: { bg: 'rgba(100,116,139,0.10)', fg: '#475569' }, - UNKNOWN: { bg: '#f1f5f9', fg: '#94a3b8' }, - }, - freshness: { - FRESH: { bg: 'rgba(26,122,74,0.10)', fg: '#1a7a4a' }, - STALE: { bg: 'rgba(217,119,6,0.10)', fg: '#d97706' }, - OUTDATED: { bg: 'rgba(192,57,43,0.10)', fg: '#c0392b' }, - }, - dataQuality: { - HIGH: { bg: 'rgba(26,122,74,0.10)', fg: '#1a7a4a' }, - MEDIUM: { bg: 'rgba(217,119,6,0.10)', fg: '#d97706' }, - LOW: { bg: 'rgba(192,57,43,0.10)', fg: '#c0392b' }, - INCOMPLETE: { bg: 'rgba(127,0,0,0.12)', fg: '#7f1d1d' }, - }, -} as const - -// ── Score → qualitative level helpers ──────────────────────────────────────── - -export function scoreToConfidenceLevel(score: number): ConfidenceLevel { - if (score >= CONF_HIGH) return 'VERY_HIGH' - if (score >= 0.75) return 'HIGH' - if (score >= CONF_MEDIUM) return 'MEDIUM' - if (score >= 0.35) return 'LOW' - return 'VERY_LOW' -} - -export function scoreToDataQualityLevel(score: number): DataQualityLevel { - if (score >= DQ_HIGH) return 'HIGH' - if (score >= DQ_MEDIUM) return 'MEDIUM' - if (score > 0) return 'LOW' - return 'INCOMPLETE' -} diff --git a/.claude/worktrees/agent-a82a3716/src/lib/locationIntelligence.ts b/.claude/worktrees/agent-a82a3716/src/lib/locationIntelligence.ts deleted file mode 100644 index 947fe1d..0000000 --- a/.claude/worktrees/agent-a82a3716/src/lib/locationIntelligence.ts +++ /dev/null @@ -1,139 +0,0 @@ -// Static location intelligence data per city — mock values based on Swiss market context - -export interface CityIntelligence { - vacancyRatePct: number // Leerstandsquote % - rentTrend12m: number // Mietpreisveränderung % (letztes Jahr) - purchasingPowerIndex: number // Kaufkraft-Index (CH = 100) - dominantIndustryClusters: string[] - plannedInfrastructure: { project: string; timeline: string; impact: string }[] - medianRentOffice: number // CHF/m² für Bürofläche - medianRentLogistics: number - medianRentRetail: number - avgDaysOnMarket: number // Durchschnittliche Tage bis Vermietung - demandStrength: 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH' - taxIndexCanton: number // Steuerindex 100 = CH-Mittel -} - -export const CITY_INTELLIGENCE: Record = { - 'Zürich': { - vacancyRatePct: 2.8, - rentTrend12m: +4.2, - purchasingPowerIndex: 128, - dominantIndustryClusters: ['Finanz & Banking', 'Tech & Startups', 'Medien & Kreativ', 'Pharma & Life Science'], - plannedInfrastructure: [ - { project: 'Tram Hardbrücke-Verlängerung', timeline: '2027', impact: 'Bessere ÖV-Anbindung Industriequartier' }, - { project: 'Rosengarten-Tunnel', timeline: '2030', impact: 'Entlastung Kreis 5/6, weniger Durchgangsverkehr' }, - ], - medianRentOffice: 42, - medianRentLogistics: 14, - medianRentRetail: 180, - avgDaysOnMarket: 38, - demandStrength: 'VERY_HIGH', - taxIndexCanton: 100, - }, - 'Basel': { - vacancyRatePct: 4.1, - rentTrend12m: +1.8, - purchasingPowerIndex: 112, - dominantIndustryClusters: ['Pharma & Chemie', 'Logistik & Handel', 'Medizintechnik', 'Finanzdienstleistungen'], - plannedInfrastructure: [ - { project: 'Basel SBB Südeingang Neubau', timeline: '2026', impact: 'Aufwertung Bahnhofumgebung' }, - { project: 'Regio-S-Bahn Ausbau', timeline: '2028', impact: 'Bessere Grenzpendler-Anbindung' }, - ], - medianRentOffice: 32, - medianRentLogistics: 11, - medianRentRetail: 120, - avgDaysOnMarket: 52, - demandStrength: 'HIGH', - taxIndexCanton: 98, - }, - 'Bern': { - vacancyRatePct: 3.5, - rentTrend12m: +2.1, - purchasingPowerIndex: 108, - dominantIndustryClusters: ['Bundesverwaltung & NPO', 'Gesundheit', 'Bildung & Forschung', 'Versicherungen'], - plannedInfrastructure: [ - { project: 'Bernmobil Netzausbau West', timeline: '2026', impact: 'Erschliessung Entwicklungsgebiet Ausserholligen' }, - ], - medianRentOffice: 28, - medianRentLogistics: 10, - medianRentRetail: 95, - avgDaysOnMarket: 61, - demandStrength: 'MEDIUM', - taxIndexCanton: 112, - }, - 'Zug': { - vacancyRatePct: 1.9, - rentTrend12m: +5.1, - purchasingPowerIndex: 148, - dominantIndustryClusters: ['Rohstoffhandel', 'Crypto & Blockchain', 'Holding & Finanzen', 'Tech-Unternehmen'], - plannedInfrastructure: [ - { project: 'Metrobahn Zug-Luzern', timeline: '2029', impact: 'Direktverbindung Luzern in 18 Min.' }, - ], - medianRentOffice: 38, - medianRentLogistics: 13, - medianRentRetail: 140, - avgDaysOnMarket: 24, - demandStrength: 'VERY_HIGH', - taxIndexCanton: 60, - }, - 'Winterthur': { - vacancyRatePct: 5.8, - rentTrend12m: +0.9, - purchasingPowerIndex: 98, - dominantIndustryClusters: ['Industrie & Maschinenbau', 'Logistik', 'Gesundheit & Soziales'], - plannedInfrastructure: [ - { project: 'Stadtraum HB Winterthur', timeline: '2027', impact: 'Aufwertung Bahnhofsumgebung, mehr Frequenz' }, - ], - medianRentOffice: 22, - medianRentLogistics: 9, - medianRentRetail: 75, - avgDaysOnMarket: 74, - demandStrength: 'MEDIUM', - taxIndexCanton: 119, - }, - 'Geneva': { - vacancyRatePct: 2.2, - rentTrend12m: +3.6, - purchasingPowerIndex: 135, - dominantIndustryClusters: ['Internationale Organisationen', 'Luxusgüter', 'Banking & Private Equity', 'Uhrenindustrie'], - plannedInfrastructure: [ - { project: 'CEVA Linie Verlängerung', timeline: '2026', impact: 'Bessere Verbindung Lancy-Pont-Rouge' }, - ], - medianRentOffice: 55, - medianRentLogistics: 18, - medianRentRetail: 220, - avgDaysOnMarket: 31, - demandStrength: 'HIGH', - taxIndexCanton: 125, - }, - 'St.Gallen': { - vacancyRatePct: 6.2, - rentTrend12m: -0.5, - purchasingPowerIndex: 95, - dominantIndustryClusters: ['Textil & Mode', 'KMU', 'Logistik', 'Gesundheit'], - plannedInfrastructure: [], - medianRentOffice: 19, - medianRentLogistics: 8, - medianRentRetail: 65, - avgDaysOnMarket: 88, - demandStrength: 'LOW', - taxIndexCanton: 107, - }, -} - -export function getCityIntelligence(city: string): CityIntelligence | null { - // Try exact match first, then partial - if (CITY_INTELLIGENCE[city]) return CITY_INTELLIGENCE[city] - const key = Object.keys(CITY_INTELLIGENCE).find(k => city.toLowerCase().includes(k.toLowerCase())) - return key ? CITY_INTELLIGENCE[key] : null -} - -export function getMarketRent(city: string, assetType: string): number | null { - const intel = getCityIntelligence(city) - if (!intel) return null - if (assetType === 'OFFICE') return intel.medianRentOffice - if (assetType === 'LOGISTICS' || assetType === 'LIGHT_INDUSTRIAL') return intel.medianRentLogistics - if (assetType === 'RETAIL') return intel.medianRentRetail - return intel.medianRentOffice -} diff --git a/.claude/worktrees/agent-a82a3716/src/lib/mockUtils.ts b/.claude/worktrees/agent-a82a3716/src/lib/mockUtils.ts deleted file mode 100644 index 2ad46a2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/lib/mockUtils.ts +++ /dev/null @@ -1 +0,0 @@ -export const mockDelay = (ms = 150) => new Promise(res => setTimeout(res, ms)) diff --git a/.claude/worktrees/agent-a82a3716/src/lib/permissions.ts b/.claude/worktrees/agent-a82a3716/src/lib/permissions.ts deleted file mode 100644 index 0de9c2c..0000000 --- a/.claude/worktrees/agent-a82a3716/src/lib/permissions.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { MockUser } from '../stores/sessionStore' -import { UserRole, WorkspaceType, ResultType } from '../domain/enums' - -// ── Permission Strings ──────────────────────────────────────────────────────── - -export const Permission = { - SUPPLY_VIEW: 'supply:view', - SUPPLY_EDIT: 'supply:edit', - DEMAND_VIEW: 'demand:view', - DEMAND_EDIT: 'demand:edit', - DEMAND_REQUEST_CONTACT: 'demand:request_contact', - OPS_VIEW: 'ops:view', - OPS_REVIEW: 'ops:review', - OPS_APPROVE: 'ops:approve', - FUTURE_SIGNAL_VIEW: 'future_signal:view', - FUTURE_SIGNAL_REVIEW: 'future_signal:review', - CONTACT_RELEASE_REQUEST: 'contact_release:request', - CONTACT_RELEASE_APPROVE: 'contact_release:approve', -} as const -export type Permission = typeof Permission[keyof typeof Permission] - -// ── Role → Permission Matrix ────────────────────────────────────────────────── - -const ALL_PERMISSIONS = Object.values(Permission) - -const ROLE_PERMISSIONS: Record = { - [UserRole.SUPER_ADMIN]: ALL_PERMISSIONS, - [UserRole.ORGANIZATION_ADMIN]: ALL_PERMISSIONS, - [UserRole.PROPERTY_MANAGER]: [ - Permission.SUPPLY_VIEW, - Permission.SUPPLY_EDIT, - Permission.DEMAND_VIEW, - Permission.FUTURE_SIGNAL_VIEW, - Permission.FUTURE_SIGNAL_REVIEW, - Permission.CONTACT_RELEASE_APPROVE, - ], - [UserRole.REVIEWER]: [ - Permission.OPS_VIEW, - Permission.OPS_REVIEW, - Permission.FUTURE_SIGNAL_VIEW, - Permission.FUTURE_SIGNAL_REVIEW, - ], - [UserRole.OWNER_VIEWER]: [ - Permission.SUPPLY_VIEW, - Permission.CONTACT_RELEASE_APPROVE, - ], - [UserRole.DEMAND_USER]: [ - Permission.DEMAND_VIEW, - Permission.DEMAND_EDIT, - Permission.DEMAND_REQUEST_CONTACT, - Permission.CONTACT_RELEASE_REQUEST, - ], -} - -// ── Role → Workspace Access ─────────────────────────────────────────────────── - -const WORKSPACE_ROLES: Record = { - [WorkspaceType.SUPPLY]: [ - UserRole.SUPER_ADMIN, - UserRole.ORGANIZATION_ADMIN, - UserRole.PROPERTY_MANAGER, - UserRole.OWNER_VIEWER, - ], - [WorkspaceType.DEMAND]: [ - UserRole.SUPER_ADMIN, - UserRole.ORGANIZATION_ADMIN, - UserRole.PROPERTY_MANAGER, - UserRole.DEMAND_USER, - ], - [WorkspaceType.OPERATIONS]: [ - UserRole.SUPER_ADMIN, - UserRole.REVIEWER, - ], -} - -// ── Core Functions ──────────────────────────────────────────────────────────── - -export function getPermissions(user: MockUser): Permission[] { - return ROLE_PERMISSIONS[user.role] ?? [] -} - -export function hasPermission(user: MockUser, permission: Permission): boolean { - return getPermissions(user).includes(permission) -} - -export function canAccessWorkspace(user: MockUser, workspace: WorkspaceType): boolean { - return WORKSPACE_ROLES[workspace].includes(user.role) -} - -export function getAccessibleWorkspaces(role: UserRole): WorkspaceType[] { - return Object.entries(WORKSPACE_ROLES) - .filter(([, roles]) => roles.includes(role)) - .map(([ws]) => ws as WorkspaceType) -} - -// ── Domain Permission Functions ─────────────────────────────────────────────── - -export function canViewProperty( - user: MockUser, - property: { organizationId: string; resultType: ResultType }, -): boolean { - if (user.role === UserRole.SUPER_ADMIN) return true - if (property.resultType === ResultType.VERIFIED_PORTFOLIO) { - return property.organizationId === user.organizationId - } - if (property.resultType === ResultType.EXTERNAL_MARKET) return true - if (property.resultType === ResultType.FUTURE_AVAILABILITY) { - return hasPermission(user, Permission.FUTURE_SIGNAL_VIEW) - } - return false -} - -export function canViewMatch( - user: MockUser, - match: { organizationId?: string }, -): boolean { - if (user.role === UserRole.SUPER_ADMIN || user.role === UserRole.ORGANIZATION_ADMIN) return true - if (user.role === UserRole.PROPERTY_MANAGER || user.role === UserRole.REVIEWER) return true - if (user.role === UserRole.DEMAND_USER) return match.organizationId === user.organizationId - return false // OWNER_VIEWER: only released matches — handled at component level -} - -export function canReviewFutureSignal(user: MockUser): boolean { - return hasPermission(user, Permission.FUTURE_SIGNAL_REVIEW) -} - -export function canApproveContactRelease(user: MockUser): boolean { - return hasPermission(user, Permission.CONTACT_RELEASE_APPROVE) -} - -export function canRequestContactRelease(user: MockUser): boolean { - return hasPermission(user, Permission.CONTACT_RELEASE_REQUEST) -} diff --git a/.claude/worktrees/agent-a82a3716/src/lib/theme.ts b/.claude/worktrees/agent-a82a3716/src/lib/theme.ts deleted file mode 100644 index be22b6c..0000000 --- a/.claude/worktrees/agent-a82a3716/src/lib/theme.ts +++ /dev/null @@ -1,110 +0,0 @@ -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' }, - contained: { - '&.MuiButton-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/.claude/worktrees/agent-a82a3716/src/lib/utils.ts b/.claude/worktrees/agent-a82a3716/src/lib/utils.ts deleted file mode 100644 index 95fc43d..0000000 --- a/.claude/worktrees/agent-a82a3716/src/lib/utils.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { - DQ_HIGH, DQ_MEDIUM, - CONF_HIGH, CONF_MEDIUM, - SCORE_STRONG, SCORE_MODERATE, - PROB_HIGH, PROB_MEDIUM, -} from './constants' - -// ── Formatting ──────────────────────────────────────────────────────────────── - -export function formatCHF(amount: number, decimals = 0): string { - return new Intl.NumberFormat('de-CH', { - style: 'currency', - currency: 'CHF', - minimumFractionDigits: decimals, - maximumFractionDigits: decimals, - }).format(amount) -} - -export function formatArea(sqm: number): string { - return `${new Intl.NumberFormat('de-CH').format(sqm)} m²` -} - -export function formatPercent(value: number, decimals = 0): string { - return `${(value * 100).toFixed(decimals)} %` -} - -export function formatDate(iso: string): string { - return new Intl.DateTimeFormat('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(iso)) -} - -export function formatRelativeDate(iso: string): string { - const diff = Date.now() - new Date(iso).getTime() - const hours = Math.floor(diff / 3_600_000) - if (hours < 1) return 'Gerade eben' - if (hours < 24) return `vor ${hours} Stunde${hours === 1 ? '' : 'n'}` - const days = Math.floor(hours / 24) - if (days < 7) return `vor ${days} Tag${days === 1 ? '' : 'en'}` - return formatDate(iso) -} - -// ── Color helpers (return MUI color token strings) ──────────────────────────── - -export function dataQualityColor(score: number): 'success' | 'warning' | 'error' { - if (score >= DQ_HIGH) return 'success' - if (score >= DQ_MEDIUM) return 'warning' - return 'error' -} - -export function confidenceColor(score: number): 'success' | 'primary' | 'warning' { - if (score >= CONF_HIGH) return 'success' - if (score >= CONF_MEDIUM) return 'primary' - return 'warning' -} - -export function matchScoreColor(score: number): 'success' | 'warning' | 'error' { - if (score >= SCORE_STRONG) return 'success' - if (score >= SCORE_MODERATE) return 'warning' - return 'error' -} - -export function probabilityColor(prob: number): 'success' | 'warning' | 'error' { - if (prob >= PROB_HIGH) return 'success' - if (prob >= PROB_MEDIUM) return 'warning' - return 'error' -} - -// Hex color variants for use in sx (when MUI color tokens aren't enough) -export function dataQualityHex(score: number): string { - if (score >= DQ_HIGH) return '#1a7a4a' - if (score >= DQ_MEDIUM) return '#d97706' - return '#c0392b' -} - -export function confidenceHex(score: number): string { - if (score >= CONF_HIGH) return '#1a7a4a' - if (score >= CONF_MEDIUM) return '#1e3a5f' - return '#d97706' -} - -export function probabilityHex(prob: number): string { - if (prob >= PROB_HIGH) return '#1a7a4a' - if (prob >= PROB_MEDIUM) return '#d97706' - return '#c0392b' -} - -// ── Misc ────────────────────────────────────────────────────────────────────── - -export function clamp(value: number, min: number, max: number): number { - return Math.min(Math.max(value, min), max) -} - -export function average(values: number[]): number { - if (values.length === 0) return 0 - return values.reduce((a, b) => a + b, 0) / values.length -} - -export function truncate(str: string, maxLen: number): string { - return str.length <= maxLen ? str : str.slice(0, maxLen - 1) + '…' -} diff --git a/.claude/worktrees/agent-a82a3716/src/main.tsx b/.claude/worktrees/agent-a82a3716/src/main.tsx deleted file mode 100644 index 324e47e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/main.tsx +++ /dev/null @@ -1,36 +0,0 @@ -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 { AuthProvider } from './provider/AuthProvider' -import { STALE_PROPERTIES } from './lib/constants' -import './index.css' -import App from './App.tsx' - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: STALE_PROPERTIES, - retry: 1, - }, - }, -}) - -createRoot(document.getElementById('root')!).render( - - - - - - - - - - - - - - , -) diff --git a/.claude/worktrees/agent-a82a3716/src/mock-data/aiOutputs.ts b/.claude/worktrees/agent-a82a3716/src/mock-data/aiOutputs.ts deleted file mode 100644 index 6c04ac0..0000000 --- a/.claude/worktrees/agent-a82a3716/src/mock-data/aiOutputs.ts +++ /dev/null @@ -1,243 +0,0 @@ -import type { AIOutput } from '../domain/aiOutput' - -export const mockAIOutputs: AIOutput[] = [ - { - id: 'aio-001', - type: 'NEED_PARSE', - provider: 'anthropic', - model: 'claude-3-5-sonnet-20241022', - promptVersion: 'need-parse-v2.3', - schemaVersion: 'schema-v4', - inputHash: 'a7f3b2c1', - outputPreview: '{"criteria":{"area":{"min":300,"max":800},"location":"Zürich","type":"Büro","budget":{"max":12000}},"confidence":0.94}', - createdAt: '2026-05-17T10:24:00Z', - latencyMs: 1240, - costEstimate: 0.0034, - reviewStatus: 'UNREVIEWED', - relatedEntityType: 'NEED', - relatedEntityId: 'need-001', - }, - { - id: 'aio-002', - type: 'MATCH_EXPLANATION', - provider: 'anthropic', - model: 'claude-3-5-sonnet-20241022', - promptVersion: 'match-explain-v1.8', - schemaVersion: 'schema-v3', - inputHash: 'c8d4e9f2', - outputPreview: 'Dieses Objekt erfüllt 4 von 5 Hardkriterien: Fläche 450m² (✓), Lage Zürich-Innenstadt (✓), Budget CHF 9\'500/Mt (✓), Parkplätze 2/3 (✗), Verfügbarkeit Q3 2026 (✓).', - createdAt: '2026-05-17T09:15:00Z', - latencyMs: 2100, - costEstimate: 0.0089, - reviewStatus: 'APPROVED', - relatedEntityType: 'MATCH', - relatedEntityId: 'match-003', - }, - { - id: 'aio-003', - type: 'DATA_QUALITY_SUMMARY', - provider: 'anthropic', - model: 'claude-3-haiku-20240307', - promptVersion: 'dq-summary-v1.2', - schemaVersion: 'schema-v2', - inputHash: 'f1a2b3c4', - outputPreview: '[FEHLER: Antwort nach 8.5s unterbrochen]', - createdAt: '2026-05-17T08:45:00Z', - latencyMs: 8500, - reviewStatus: 'FLAGGED', - relatedEntityType: 'PROPERTY', - relatedEntityId: 'prop-007', - error: { - type: 'PROVIDER_TIMEOUT', - message: 'Request timed out after 8500ms. Provider did not respond within the allowed window.', - recoverable: true, - }, - }, - { - id: 'aio-004', - type: 'FOLLOW_UP_QUESTIONS', - provider: 'anthropic', - model: 'claude-3-5-sonnet-20241022', - promptVersion: 'follow-up-v1.5', - schemaVersion: 'schema-v3', - inputHash: 'b2c9d7e3', - outputPreview: '["Welche Nutzungsart bevorzugen Sie: Open Space oder Einzelbüros?","Ist ein Außenbereich oder Dachterrasse gewünscht?","Bis wann benötigen Sie die Fläche?"]', - createdAt: '2026-05-17T08:02:00Z', - latencyMs: 890, - costEstimate: 0.0021, - reviewStatus: 'UNREVIEWED', - relatedEntityType: 'NEED', - relatedEntityId: 'need-002', - }, - { - id: 'aio-005', - type: 'COMPARE_SUMMARY', - provider: 'anthropic', - model: 'claude-3-5-sonnet-20241022', - promptVersion: 'compare-v2.0', - schemaVersion: 'schema-v4', - inputHash: 'e4f5a6b7', - outputPreview: 'Vergleich prop-001 vs prop-003: prop-001 bietet 15% mehr Fläche (+90m²), jedoch CHF 800/Mt höhere Mietkosten. prop-003 überzeugt durch Lage und Ausbaustandard.', - createdAt: '2026-05-16T16:30:00Z', - latencyMs: 3200, - costEstimate: 0.0122, - reviewStatus: 'IN_REVIEW', - relatedEntityType: 'MATCH', - relatedEntityId: 'match-001', - }, - { - id: 'aio-006', - type: 'DECISION_BRIEF', - provider: 'anthropic', - model: 'claude-3-opus-20240229', - promptVersion: 'decision-v1.1', - schemaVersion: 'schema-v2', - inputHash: 'c3d2e1f0', - outputPreview: 'Empfehlung: prop-002 priorisieren. Höchste Gesamtkongruenz (87%), einziges Objekt mit Außenfläche (250m²). Risiko: Mietpreiserhöhung +5% ab 2027 gemäß Mietvertrag.', - createdAt: '2026-05-16T14:10:00Z', - latencyMs: 4800, - costEstimate: 0.0341, - reviewStatus: 'UNREVIEWED', - relatedEntityType: 'MATCH', - relatedEntityId: 'match-005', - }, - { - id: 'aio-007', - type: 'MATCH_EXPLANATION', - provider: 'anthropic', - model: 'claude-3-5-sonnet-20241022', - promptVersion: 'match-explain-v1.8', - schemaVersion: 'schema-v3', - inputHash: 'd9e8f7a6', - outputPreview: '[Schema-Validierung fehlgeschlagen: Pflichtfeld \'hardCriteria\' nicht vorhanden im Output]', - createdAt: '2026-05-16T11:55:00Z', - latencyMs: 1850, - reviewStatus: 'REJECTED', - relatedEntityType: 'MATCH', - relatedEntityId: 'match-002', - error: { - type: 'SCHEMA_VALIDATION', - message: 'Output schema validation failed: required field \'hardCriteria\' missing. Output was not delivered to UI.', - recoverable: false, - }, - }, - { - id: 'aio-008', - type: 'NEED_PARSE', - provider: 'anthropic', - model: 'claude-3-haiku-20240307', - promptVersion: 'need-parse-v2.2', - schemaVersion: 'schema-v4', - inputHash: 'a1b2c3d4', - outputPreview: '{"criteria":{"area":{"min":500},"location":"Bern","type":"Logistik","budget":{"max":8000}},"confidence":0.91}', - createdAt: '2026-05-16T09:40:00Z', - latencyMs: 560, - costEstimate: 0.0009, - reviewStatus: 'APPROVED', - relatedEntityType: 'NEED', - relatedEntityId: 'need-003', - }, - { - id: 'aio-009', - type: 'DATA_QUALITY_SUMMARY', - provider: 'anthropic', - model: 'claude-3-haiku-20240307', - promptVersion: 'dq-summary-v1.2', - schemaVersion: 'schema-v2', - inputHash: 'f8e7d6c5', - outputPreview: 'Qualitätsscore: 72%. Fehlende Felder: Mietpreis/m² (kritisch), letzte Aktualisierung > 6 Monate. Empfehlung: Aktualisierung anfordern.', - createdAt: '2026-05-16T08:15:00Z', - latencyMs: 1100, - costEstimate: 0.0018, - reviewStatus: 'UNREVIEWED', - relatedEntityType: 'PROPERTY', - relatedEntityId: 'prop-003', - }, - { - id: 'aio-010', - type: 'COMPARE_SUMMARY', - provider: 'anthropic', - model: 'claude-3-5-sonnet-20241022', - promptVersion: 'compare-v2.0', - schemaVersion: 'schema-v4', - inputHash: 'b5c4d3e2', - outputPreview: '[Invalid JSON: unexpected token at position 142 — output truncated mid-generation]', - createdAt: '2026-05-15T17:20:00Z', - latencyMs: 2900, - reviewStatus: 'FLAGGED', - relatedEntityType: 'MATCH', - relatedEntityId: 'match-007', - error: { - type: 'INVALID_JSON', - message: 'Response contained malformed JSON: unexpected token at position 142. Likely caused by mid-stream truncation.', - recoverable: true, - }, - }, - { - id: 'aio-011', - type: 'FOLLOW_UP_QUESTIONS', - provider: 'anthropic', - model: 'claude-3-5-sonnet-20241022', - promptVersion: 'follow-up-v1.5', - schemaVersion: 'schema-v3', - inputHash: 'c6d5e4f3', - outputPreview: '["Welche ÖPNV-Anbindung ist Mindestanforderung?","Benötigen Sie eigene Ladeinfrastruktur für E-Fahrzeuge?","Ist Co-Working-Anteil vorstellbar?"]', - createdAt: '2026-05-15T14:50:00Z', - latencyMs: 1450, - costEstimate: 0.0028, - reviewStatus: 'IN_REVIEW', - relatedEntityType: 'NEED', - relatedEntityId: 'need-004', - }, - { - id: 'aio-012', - type: 'DECISION_BRIEF', - provider: 'anthropic', - model: 'claude-3-5-sonnet-20241022', - promptVersion: 'decision-v1.2', - schemaVersion: 'schema-v2', - inputHash: 'd7e6f5a4', - outputPreview: 'Empfehlung: Angebot annehmen. Match-Score 91%, alle Hardkriterien erfüllt. Fläche 680m² entspricht Profil (600–750m²). Nächste Schritte: Kontaktfreigabe beantragen.', - createdAt: '2026-05-15T11:30:00Z', - latencyMs: 3900, - costEstimate: 0.0198, - reviewStatus: 'APPROVED', - relatedEntityType: 'MATCH', - relatedEntityId: 'match-004', - }, - { - id: 'aio-013', - type: 'NEED_PARSE', - provider: 'anthropic', - model: 'claude-3-5-sonnet-20241022', - promptVersion: 'need-parse-v2.3', - schemaVersion: 'schema-v4', - inputHash: 'e8f7a6b5', - outputPreview: '[Leere Antwort empfangen — kein Output generiert]', - createdAt: '2026-05-15T09:05:00Z', - reviewStatus: 'FLAGGED', - relatedEntityType: 'NEED', - relatedEntityId: 'need-005', - error: { - type: 'EMPTY_RESPONSE', - message: 'Provider returned an empty response body. No tokens were generated. Request may have been filtered.', - recoverable: true, - }, - }, - { - id: 'aio-014', - type: 'MATCH_EXPLANATION', - provider: 'anthropic', - model: 'claude-3-opus-20240229', - promptVersion: 'match-explain-v1.9', - schemaVersion: 'schema-v3', - inputHash: 'f9a8b7c6', - outputPreview: 'Detailbegründung: Bürofläche 520m² entspricht exakt dem Suchprofil (500–600m²). Mietpreis CHF 11\'200/Mt liegt 6.7% über Budget, jedoch kompensiert durch Lagequalität Zürich City.', - createdAt: '2026-05-15T08:00:00Z', - latencyMs: 5200, - costEstimate: 0.0412, - reviewStatus: 'UNREVIEWED', - relatedEntityType: 'MATCH', - relatedEntityId: 'match-009', - }, -] diff --git a/.claude/worktrees/agent-a82a3716/src/mock-data/dataSources.ts b/.claude/worktrees/agent-a82a3716/src/mock-data/dataSources.ts deleted file mode 100644 index e2051be..0000000 --- a/.claude/worktrees/agent-a82a3716/src/mock-data/dataSources.ts +++ /dev/null @@ -1,302 +0,0 @@ -import type { DataSource, ConnectorRun } from '../domain/dataSource' -import { - DataSourceType, - SourceStatus, - TermsStatus, - ConnectorRunStatus, -} from '../domain/dataSource' -import { FreshnessStatus } from '../domain/enums' - -export const MOCK_DATA_SOURCES: DataSource[] = [ - { - id: 'src-001', - name: 'ImmoScout24 – Zürich/Bern Feed', - sourceType: DataSourceType.PUBLIC_WEB_SOURCE, - legalBasis: 'Öffentlich zugängliche Listings – keine personenbezogenen Daten', - termsStatus: TermsStatus.NEEDS_LEGAL_REVIEW, - dataCategories: ['Gewerbeimmobilien', 'Mietpreise', 'Verfügbarkeit'], - supportedAssetTypes: ['OFFICE', 'RETAIL', 'LIGHT_INDUSTRIAL'], - regionCoverage: ['Zürich', 'Bern', 'Basel'], - reliabilityScore: 0.72, - freshnessStatus: FreshnessStatus.FRESH, - lastRunAt: '2026-05-15T06:00:00Z', - nextRunAt: '2026-05-16T06:00:00Z', - status: SourceStatus.ACTIVE, - notes: 'Daily refresh via FUTURE_CRAWLER_STUB. Rechtliche Freigabe ausstehend.', - }, - { - id: 'src-002', - name: 'Ideal Sharing Portfolio Export', - sourceType: DataSourceType.INTERNAL_PORTFOLIO_EXPORT, - ownerOrganizationId: 'org-001', - legalBasis: 'Internes Dateneigentum – vollständig genehmigt', - termsStatus: TermsStatus.APPROVED, - dataCategories: ['Portoflio', 'Mietverträge', 'Flächen'], - supportedAssetTypes: ['OFFICE', 'LOGISTICS', 'MIXED'], - regionCoverage: ['Zürich', 'Zug', 'Luzern'], - reliabilityScore: 0.97, - freshnessStatus: FreshnessStatus.FRESH, - lastRunAt: '2026-05-15T02:00:00Z', - nextRunAt: '2026-05-15T14:00:00Z', - status: SourceStatus.ACTIVE, - }, - { - id: 'src-003', - name: 'CBRE Market Data API', - sourceType: DataSourceType.API_CONNECTOR, - ownerOrganizationId: 'org-002', - legalBasis: 'API-Lizenzvertrag mit CBRE vom 12.01.2026', - termsStatus: TermsStatus.APPROVED, - dataCategories: ['Marktdaten', 'Mietindizes', 'Transaktionsvolumen'], - supportedAssetTypes: ['OFFICE', 'RETAIL', 'LOGISTICS'], - regionCoverage: ['Schweiz', 'DACH'], - reliabilityScore: 0.91, - freshnessStatus: FreshnessStatus.STALE, - lastRunAt: '2026-05-13T10:00:00Z', - nextRunAt: '2026-05-17T10:00:00Z', - status: SourceStatus.ACTIVE, - notes: 'Wöchentlicher Refresh – Quarterly Report verfügbar.', - }, - { - id: 'src-004', - name: 'Handelsregister CH – Firmenumzüge', - sourceType: DataSourceType.PUBLIC_WEB_SOURCE, - legalBasis: 'Öffentliches Staatsregister (SHAB)', - termsStatus: TermsStatus.APPROVED, - dataCategories: ['Firmensitze', 'Umzüge', 'Neugründungen'], - supportedAssetTypes: ['OFFICE', 'MIXED'], - regionCoverage: ['Schweiz'], - reliabilityScore: 0.88, - freshnessStatus: FreshnessStatus.FRESH, - lastRunAt: '2026-05-15T03:30:00Z', - nextRunAt: '2026-05-16T03:30:00Z', - status: SourceStatus.ACTIVE, - }, - { - id: 'src-005', - name: 'Baubewilligungsregister Kt. Zürich', - sourceType: DataSourceType.FUTURE_CRAWLER_STUB, - legalBasis: 'Amtliche Publikation – öffentlich zugänglich', - termsStatus: TermsStatus.UNKNOWN, - dataCategories: ['Baubewilligungen', 'Umnutzungen', 'Abbrüche'], - supportedAssetTypes: ['OFFICE', 'PRODUCTION', 'LIGHT_INDUSTRIAL'], - regionCoverage: ['Kanton Zürich'], - reliabilityScore: 0.65, - freshnessStatus: FreshnessStatus.OUTDATED, - lastRunAt: '2026-04-30T08:00:00Z', - status: SourceStatus.PENDING_REVIEW, - notes: 'Crawler-Implementierung noch ausstehend. Manuelle Prüfung erforderlich.', - }, - { - id: 'src-006', - name: 'Mietvertragsdaten (CSV Upload – Q1 2026)', - sourceType: DataSourceType.CSV_IMPORT, - ownerOrganizationId: 'org-001', - legalBasis: 'Interner Upload durch Portfoliomanager – DSGVO-konform', - termsStatus: TermsStatus.APPROVED, - dataCategories: ['Mietverträge', 'Laufzeiten', 'Mieter'], - supportedAssetTypes: ['OFFICE', 'RETAIL'], - regionCoverage: ['Zürich', 'Basel'], - reliabilityScore: 0.84, - freshnessStatus: FreshnessStatus.STALE, - lastRunAt: '2026-03-31T09:00:00Z', - status: SourceStatus.PAUSED, - notes: 'Q2-Upload ausstehend. Quelle pausiert bis neue Datei verfügbar.', - }, - { - id: 'src-007', - name: 'JLL Research Partner Feed', - sourceType: DataSourceType.PARTNER_FEED, - ownerOrganizationId: 'org-003', - legalBasis: 'Datenaustauschabkommen mit JLL Schweiz AG – vertraulich', - termsStatus: TermsStatus.RESTRICTED, - dataCategories: ['Marktberichte', 'Leerstandsquoten', 'Prime Rents'], - supportedAssetTypes: ['OFFICE', 'LOGISTICS'], - regionCoverage: ['Zürich', 'Genf', 'Basel'], - reliabilityScore: 0.93, - freshnessStatus: FreshnessStatus.FRESH, - lastRunAt: '2026-05-15T07:00:00Z', - nextRunAt: '2026-05-22T07:00:00Z', - status: SourceStatus.ACTIVE, - notes: 'Wöchentlicher Push durch JLL. Nur für interne Nutzung – kein Re-Export.', - }, - { - id: 'src-008', - name: 'Analyst-Eingaben (Team Zürich)', - sourceType: DataSourceType.ANALYST_ENTRY, - ownerOrganizationId: 'org-001', - legalBasis: 'Interne Datenerhebung durch Analysten-Team', - termsStatus: TermsStatus.APPROVED, - dataCategories: ['Marktsignale', 'Qualitative Einschätzungen', 'Netzwerkinfos'], - supportedAssetTypes: ['OFFICE', 'RETAIL', 'MIXED'], - regionCoverage: ['Zürich', 'Winterthur'], - reliabilityScore: 0.78, - freshnessStatus: FreshnessStatus.FRESH, - lastRunAt: '2026-05-14T16:45:00Z', - status: SourceStatus.ACTIVE, - }, - { - id: 'src-009', - name: 'Homegate Gewerbe-Scraper (Beta)', - sourceType: DataSourceType.FUTURE_CRAWLER_STUB, - legalBasis: 'In rechtlicher Prüfung – noch nicht freigegeben', - termsStatus: TermsStatus.BLOCKED, - dataCategories: ['Gewerbelistings', 'Mietpreise'], - supportedAssetTypes: ['OFFICE', 'RETAIL'], - regionCoverage: ['Schweiz'], - reliabilityScore: 0.0, - freshnessStatus: FreshnessStatus.OUTDATED, - status: SourceStatus.DISABLED, - errorState: 'Quelle gesperrt – robots.txt-Prüfung negativ, AGB untersagt automatisiertes Crawling.', - }, -] - -export const MOCK_CONNECTOR_RUNS: ConnectorRun[] = [ - // src-001 runs - { - id: 'run-001-a', - sourceId: 'src-001', - startedAt: '2026-05-15T06:00:00Z', - finishedAt: '2026-05-15T06:12:34Z', - status: ConnectorRunStatus.COMPLETED, - itemsDetected: 342, - itemsNormalized: 318, - itemsRejected: 24, - signalsCreated: 7, - errors: [], - warnings: ['24 Einträge ohne gültige Flächenangabe übersprungen'], - runSummary: '342 Listings importiert. 318 normalisiert, 7 neue Marktsignale erkannt.', - }, - { - id: 'run-001-b', - sourceId: 'src-001', - startedAt: '2026-05-14T06:00:00Z', - finishedAt: '2026-05-14T06:09:11Z', - status: ConnectorRunStatus.COMPLETED, - itemsDetected: 289, - itemsNormalized: 277, - itemsRejected: 12, - signalsCreated: 4, - errors: [], - warnings: [], - runSummary: '289 Listings importiert. 4 neue Marktsignale erkannt.', - }, - { - id: 'run-001-c', - sourceId: 'src-001', - startedAt: '2026-05-13T06:00:00Z', - finishedAt: '2026-05-13T06:04:22Z', - status: ConnectorRunStatus.PARTIAL, - itemsDetected: 310, - itemsNormalized: 201, - itemsRejected: 109, - signalsCreated: 2, - errors: ['HTTP 429 nach 201 Einträgen – Rate Limit erreicht'], - warnings: ['109 Einträge konnten nicht abgerufen werden'], - runSummary: 'Teilimport wegen Rate Limiting. 201 von 310 Einträgen verarbeitet.', - }, - // src-002 runs - { - id: 'run-002-a', - sourceId: 'src-002', - startedAt: '2026-05-15T02:00:00Z', - finishedAt: '2026-05-15T02:03:08Z', - status: ConnectorRunStatus.COMPLETED, - itemsDetected: 1240, - itemsNormalized: 1240, - itemsRejected: 0, - signalsCreated: 12, - errors: [], - warnings: [], - runSummary: 'Vollständiger Portfolio-Sync. 12 Lease-Expiry-Signale erzeugt.', - }, - { - id: 'run-002-b', - sourceId: 'src-002', - startedAt: '2026-05-14T14:00:00Z', - finishedAt: '2026-05-14T14:02:55Z', - status: ConnectorRunStatus.COMPLETED, - itemsDetected: 1238, - itemsNormalized: 1238, - itemsRejected: 0, - signalsCreated: 3, - errors: [], - warnings: [], - runSummary: 'Delta-Sync erfolgreich. 3 neue Objekte hinzugefügt.', - }, - // src-003 runs - { - id: 'run-003-a', - sourceId: 'src-003', - startedAt: '2026-05-13T10:00:00Z', - finishedAt: '2026-05-13T10:18:42Z', - status: ConnectorRunStatus.COMPLETED, - itemsDetected: 86, - itemsNormalized: 86, - itemsRejected: 0, - signalsCreated: 0, - errors: [], - warnings: ['Quartalsdaten verfügbar – manueller Review empfohlen'], - runSummary: 'Marktdaten-Update Q1 2026 importiert. 86 Datenpunkte aktualisiert.', - }, - // src-004 runs - { - id: 'run-004-a', - sourceId: 'src-004', - startedAt: '2026-05-15T03:30:00Z', - finishedAt: '2026-05-15T03:44:17Z', - status: ConnectorRunStatus.COMPLETED, - itemsDetected: 523, - itemsNormalized: 498, - itemsRejected: 25, - signalsCreated: 9, - errors: [], - warnings: ['25 Einträge ohne Adressangabe ignoriert'], - runSummary: '523 Handelsregistereinträge geprüft. 9 Firmensitz-Signale erkannt.', - }, - // src-007 runs - { - id: 'run-007-a', - sourceId: 'src-007', - startedAt: '2026-05-15T07:00:00Z', - finishedAt: '2026-05-15T07:05:30Z', - status: ConnectorRunStatus.COMPLETED, - itemsDetected: 14, - itemsNormalized: 14, - itemsRejected: 0, - signalsCreated: 3, - errors: [], - warnings: [], - runSummary: 'JLL Weekly Report verarbeitet. 3 Marktberichte als Signale importiert.', - }, - // src-008 runs - { - id: 'run-008-a', - sourceId: 'src-008', - startedAt: '2026-05-14T16:45:00Z', - finishedAt: '2026-05-14T16:45:52Z', - status: ConnectorRunStatus.COMPLETED, - itemsDetected: 3, - itemsNormalized: 3, - itemsRejected: 0, - signalsCreated: 3, - errors: [], - warnings: [], - runSummary: '3 manuelle Analysten-Einträge verarbeitet. Je 1 Signal pro Eintrag.', - }, - // src-006 – last run before pause - { - id: 'run-006-a', - sourceId: 'src-006', - startedAt: '2026-03-31T09:00:00Z', - finishedAt: '2026-03-31T09:02:19Z', - status: ConnectorRunStatus.COMPLETED, - itemsDetected: 412, - itemsNormalized: 408, - itemsRejected: 4, - signalsCreated: 18, - errors: [], - warnings: ['4 Einträge mit doppelter Vertrags-ID ignoriert'], - runSummary: 'Q1 CSV verarbeitet. 408 Mietverträge importiert, 18 Lease-Expiry-Signale.', - }, -] diff --git a/.claude/worktrees/agent-a82a3716/src/mock-data/futureSignals.ts b/.claude/worktrees/agent-a82a3716/src/mock-data/futureSignals.ts deleted file mode 100644 index 391e7fb..0000000 --- a/.claude/worktrees/agent-a82a3716/src/mock-data/futureSignals.ts +++ /dev/null @@ -1,428 +0,0 @@ -import { SignalType, RiskLevel } from '../domain/enums' -import type { FutureSignal } from '../domain/futureSignal' - -export const mockFutureSignals: FutureSignal[] = [ - // --- signal-001: DataCloud Expansion Zürich-West --- - { - 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', - }, - - // --- signal-002: Helvetia Produktion possible move-out Reinach --- - { - 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', - }, - - // --- signal-003: Bern Wankdorf Neubau Büro/Gewerbe --- - { - 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', - }, - - // --- signal-004: Pharma-Biotech Basel Expansion --- - { - id: 'signal-004', - signalType: SignalType.EXPANSION, - companyName: 'Novabio Pharma AG', - locationHint: 'Basel, Allschwil', - areaSqmEstimate: 700, - probability: 0.63, - confidenceScore: 0.60, - timeHorizonMonths: 14, - source: { - type: 'COMPANY_REPORT', - url: 'https://example.com/annual/novabio-2025', - publishedAt: '2025-03-28', - credibility: 'HIGH', - }, - sensitivityLevel: 'INTERNAL', - disclaimer: 'Signal basiert auf Geschäftsbericht und Expansionsplänen. Kein bestätigtes Mietobjekt.', - riskLevel: RiskLevel.MEDIUM, - marketIndicator: 'Pharmastandort Basel: +22% Beschäftigte Life-Sciences 2024', - relevanceScore: 0.70, - isVerified: false, - expiresAt: '2026-07-01', - organizationId: 'org-wincasa', - createdAt: '2025-04-02T09:00:00Z', - updatedAt: '2025-05-08T10:00:00Z', - }, - - // --- signal-005: Finanz AG Zürich-Nord possible move-out → prop-023 --- - { - id: 'signal-005', - signalType: SignalType.POSSIBLE_MOVE_OUT, - companyName: 'Finanz & Treuhand AG', - propertyId: 'prop-023', - locationHint: 'Zürich-Nord, Seebach', - areaSqmEstimate: 850, - probability: 0.58, - confidenceScore: 0.54, - timeHorizonMonths: 8, - source: { - type: 'MARKET_DATA', - publishedAt: '2025-04-10', - credibility: 'MEDIUM', - }, - sensitivityLevel: 'INTERNAL', - disclaimer: 'Marktdaten deuten auf mögliche Standortverlagerung hin. Kein bestätigter Auszug.', - riskLevel: RiskLevel.MEDIUM, - marketIndicator: 'Leerstand Zürich-Nord Q1 2025: +12% QoQ', - relevanceScore: 0.65, - isVerified: false, - expiresAt: '2026-01-01', - organizationId: 'org-wincasa', - createdAt: '2025-04-12T08:00:00Z', - updatedAt: '2025-05-10T09:00:00Z', - }, - - // --- signal-006: Luzern Inseli Neubau Gewerbe --- - { - id: 'signal-006', - signalType: SignalType.CONSTRUCTION_PROJECT, - locationHint: 'Luzern, Inseli-Quartier', - areaSqmEstimate: 2000, - probability: 0.78, - confidenceScore: 0.74, - timeHorizonMonths: 22, - source: { - type: 'CONSTRUCTION_PERMIT', - publishedAt: '2025-01-20', - credibility: 'HIGH', - }, - sensitivityLevel: 'PUBLIC', - disclaimer: 'Baubewilligung eingereicht. Fertigstellung ca. Q1 2027. Nutzungskonzept noch nicht endgültig.', - riskLevel: RiskLevel.LOW, - marketIndicator: 'Neubauprojekte Luzern Innenstadt 2025–2027', - relevanceScore: 0.72, - isVerified: false, - expiresAt: '2027-02-01', - organizationId: 'org-wincasa', - createdAt: '2025-01-25T11:00:00Z', - updatedAt: '2025-05-06T14:00:00Z', - }, - - // --- signal-007: E-Commerce Zug Expansion → prop-026 --- - { - id: 'signal-007', - signalType: SignalType.EXPANSION, - companyName: 'SwissCart E-Commerce GmbH', - propertyId: 'prop-026', - locationHint: 'Zug, Industriestrasse', - areaSqmEstimate: 580, - probability: 0.66, - confidenceScore: 0.62, - timeHorizonMonths: 9, - source: { - type: 'JOB_POSTING', - url: 'https://example.com/jobs/swisscart-zug', - publishedAt: '2025-04-18', - credibility: 'MEDIUM', - }, - sensitivityLevel: 'INTERNAL', - disclaimer: 'Signal basiert auf massivem Stellenaufbau. Expansion in Zug sehr wahrscheinlich, aber noch kein Mietobjekt identifiziert.', - riskLevel: RiskLevel.MEDIUM, - marketIndicator: 'E-Commerce Zug: Stellenwachstum +55% YoY', - relevanceScore: 0.69, - isVerified: false, - expiresAt: '2026-02-01', - organizationId: 'org-wincasa', - createdAt: '2025-04-20T07:00:00Z', - updatedAt: '2025-05-09T08:00:00Z', - }, - - // --- signal-008: Retail Zürich Niederdorf possible move-out → prop-025 --- - { - id: 'signal-008', - signalType: SignalType.POSSIBLE_MOVE_OUT, - companyName: 'Textilhaus Zürich AG', - propertyId: 'prop-025', - locationHint: 'Zürich Niederdorf, Münstergasse', - areaSqmEstimate: 280, - probability: 0.55, - confidenceScore: 0.50, - timeHorizonMonths: 18, - source: { - type: 'MARKET_DATA', - publishedAt: '2025-03-30', - credibility: 'MEDIUM', - }, - sensitivityLevel: 'CONFIDENTIAL', - disclaimer: 'Brancheninformationen deuten auf Verkleinerung hin. Kein bestätigter Auszug. Vertraulich.', - riskLevel: RiskLevel.MEDIUM, - marketIndicator: 'Stationärer Handel Zürich Altstadt: Leerstand +8% 2024', - relevanceScore: 0.60, - isVerified: false, - expiresAt: '2026-10-01', - organizationId: 'org-wincasa', - createdAt: '2025-04-01T10:00:00Z', - updatedAt: '2025-05-07T11:00:00Z', - }, - - // --- signal-009: Winterthur Zentrum Neubau Büro --- - { - id: 'signal-009', - signalType: SignalType.CONSTRUCTION_PROJECT, - locationHint: 'Winterthur, Zentrum Technikum', - areaSqmEstimate: 1200, - probability: 0.80, - confidenceScore: 0.76, - timeHorizonMonths: 20, - source: { - type: 'CONSTRUCTION_PERMIT', - publishedAt: '2025-02-28', - credibility: 'HIGH', - }, - sensitivityLevel: 'PUBLIC', - disclaimer: 'Baubewilligung öffentlich. Fertigstellung ca. Q2 2027.', - riskLevel: RiskLevel.LOW, - marketIndicator: 'Winterthur Stadtentwicklung: Büroflächenneubau 2025–2027', - relevanceScore: 0.75, - isVerified: true, - verifiedBy: 'admin@ideal-sharing.ch', - verifiedAt: '2025-04-10T10:00:00Z', - expiresAt: '2027-05-01', - organizationId: 'org-wincasa', - createdAt: '2025-03-05T09:00:00Z', - updatedAt: '2025-04-10T10:00:00Z', - }, - - // --- signal-010: TechHub St.Gallen Expansion → prop-030 --- - { - id: 'signal-010', - signalType: SignalType.EXPANSION, - companyName: 'Ostschweiz Digital AG', - propertyId: 'prop-030', - locationHint: 'St. Gallen, Riethüsli', - areaSqmEstimate: 480, - probability: 0.60, - confidenceScore: 0.55, - timeHorizonMonths: 10, - source: { - type: 'JOB_POSTING', - url: 'https://example.com/jobs/ostschweiz-digital', - publishedAt: '2025-04-22', - credibility: 'MEDIUM', - }, - sensitivityLevel: 'INTERNAL', - disclaimer: 'Expansion basiert auf Analyse von Stellenanzeigen und Unternehmensankündigungen. Kein bestätigtes Objekt.', - riskLevel: RiskLevel.MEDIUM, - marketIndicator: 'Digitalwirtschaft Ostschweiz: +28% Beschäftigte 2024', - relevanceScore: 0.62, - isVerified: false, - expiresAt: '2026-03-01', - organizationId: 'org-wincasa', - createdAt: '2025-04-25T08:00:00Z', - updatedAt: '2025-05-10T07:00:00Z', - }, - - // --- signal-011: Produktion Münchenbuchsee possible move-out → prop-027 --- - { - id: 'signal-011', - signalType: SignalType.POSSIBLE_MOVE_OUT, - companyName: 'Präzisionsmechanik Bern AG', - propertyId: 'prop-027', - locationHint: 'Münchenbuchsee BE, Industriezone', - areaSqmEstimate: 2200, - probability: 0.52, - confidenceScore: 0.48, - timeHorizonMonths: 14, - source: { - type: 'PRESS', - url: 'https://example.com/news/pmbern-verlagerung', - publishedAt: '2025-03-10', - credibility: 'HIGH', - }, - sensitivityLevel: 'CONFIDENTIAL', - disclaimer: 'Pressemeldungen über Verlagerung der Produktion ins Ausland. Kein bestätigter Auszug. Vertraulich behandeln.', - riskLevel: RiskLevel.HIGH, - marketIndicator: 'Verlagerungsdruck Schweizer Maschinenbau 2025', - relevanceScore: 0.58, - isVerified: false, - expiresAt: '2026-08-01', - organizationId: 'org-wincasa', - createdAt: '2025-03-12T10:00:00Z', - updatedAt: '2025-05-09T09:00:00Z', - }, - - // --- signal-012: Basel Hafen Neubau Logistik → prop-024 --- - { - id: 'signal-012', - signalType: SignalType.CONSTRUCTION_PROJECT, - propertyId: 'prop-024', - locationHint: 'Basel, Hafen Klybeck', - areaSqmEstimate: 2600, - probability: 0.82, - confidenceScore: 0.78, - timeHorizonMonths: 12, - source: { - type: 'CONSTRUCTION_PERMIT', - publishedAt: '2025-01-15', - credibility: 'HIGH', - }, - sensitivityLevel: 'PUBLIC', - disclaimer: 'Baubewilligung erteilt. Logistikneubau am Rheinhafen. Fertigstellung gemäss Baugesuch Q2 2026.', - riskLevel: RiskLevel.LOW, - marketIndicator: 'Hafenerweiterung Basel Klybeck: Logistikflächen 2026', - relevanceScore: 0.80, - isVerified: true, - verifiedBy: 'admin@ideal-sharing.ch', - verifiedAt: '2025-04-20T14:00:00Z', - expiresAt: '2026-08-01', - organizationId: 'org-wincasa', - createdAt: '2025-01-18T10:00:00Z', - updatedAt: '2025-04-20T14:00:00Z', - }, - - // --- signal-013: Genf La Praille Office Expansion → prop-028 --- - { - id: 'signal-013', - signalType: SignalType.EXPANSION, - companyName: 'Geneva Finance Partners SA', - propertyId: 'prop-028', - locationHint: 'Genf, La Praille', - areaSqmEstimate: 520, - probability: 0.58, - confidenceScore: 0.53, - timeHorizonMonths: 20, - source: { - type: 'COMPANY_REPORT', - url: 'https://example.com/annual/gfp-2025', - publishedAt: '2025-03-05', - credibility: 'MEDIUM', - }, - sensitivityLevel: 'INTERNAL', - disclaimer: 'Expansionspläne aus Jahresbericht. Standort La Praille wahrscheinlich, aber noch nicht definitiv.', - riskLevel: RiskLevel.MEDIUM, - marketIndicator: 'Büroflächennachfrage Genf: +15% 2025', - relevanceScore: 0.60, - isVerified: false, - expiresAt: '2027-01-01', - organizationId: 'org-wincasa', - createdAt: '2025-03-08T11:00:00Z', - updatedAt: '2025-05-07T10:00:00Z', - }, - - // --- signal-014: Frenkendorf Lager possible move-out → prop-029 --- - { - id: 'signal-014', - signalType: SignalType.POSSIBLE_MOVE_OUT, - companyName: 'Schweizer Grosshandel AG', - propertyId: 'prop-029', - locationHint: 'Frenkendorf BL, Lager Nord', - areaSqmEstimate: 3500, - probability: 0.50, - confidenceScore: 0.46, - timeHorizonMonths: 15, - source: { - type: 'MARKET_DATA', - publishedAt: '2025-04-05', - credibility: 'MEDIUM', - }, - sensitivityLevel: 'CONFIDENTIAL', - disclaimer: 'Marktdaten deuten auf mögliche Konsolidierung hin. Kein bestätigter Auszug. Vertraulich.', - riskLevel: RiskLevel.HIGH, - marketIndicator: 'Grosshandel Nordwestschweiz: Konsolidierungstrend 2025', - relevanceScore: 0.55, - isVerified: false, - expiresAt: '2026-09-01', - organizationId: 'org-wincasa', - createdAt: '2025-04-07T08:00:00Z', - updatedAt: '2025-05-08T09:00:00Z', - }, - - // --- signal-015: Bern Tech Campus Expansion --- - { - id: 'signal-015', - signalType: SignalType.EXPANSION, - companyName: 'BernTech Innovation AG', - locationHint: 'Bern, Breitenrain', - areaSqmEstimate: 1800, - probability: 0.68, - confidenceScore: 0.64, - timeHorizonMonths: 16, - source: { - type: 'JOB_POSTING', - url: 'https://example.com/jobs/berntech', - publishedAt: '2025-04-25', - credibility: 'MEDIUM', - }, - sensitivityLevel: 'INTERNAL', - disclaimer: 'Wachstumssignal aus Stellenanzeigen und Social-Media-Analyse. Kein bestätigtes Objekt.', - riskLevel: RiskLevel.MEDIUM, - marketIndicator: 'Bern Tech-Ökosystem: Risikokapital +40% 2024', - relevanceScore: 0.66, - isVerified: false, - expiresAt: '2026-10-01', - organizationId: 'org-wincasa', - createdAt: '2025-04-28T09:00:00Z', - updatedAt: '2025-05-10T08:00:00Z', - }, -] diff --git a/.claude/worktrees/agent-a82a3716/src/mock-data/index.ts b/.claude/worktrees/agent-a82a3716/src/mock-data/index.ts deleted file mode 100644 index 200e90c..0000000 --- a/.claude/worktrees/agent-a82a3716/src/mock-data/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { mockProperties } from './properties' -export { mockNeeds } from './needs' -export { mockMatches } from './matches' -export { mockFutureSignals } from './futureSignals' -export { mockShortlists } from './shortlists' -export { mockReviewQueue } from './reviewQueue' diff --git a/.claude/worktrees/agent-a82a3716/src/mock-data/marketSignals.ts b/.claude/worktrees/agent-a82a3716/src/mock-data/marketSignals.ts deleted file mode 100644 index 408ff3a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/mock-data/marketSignals.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { - MarketSignalSourceCategory, - SignalProcessingStatus, - EvidenceType, - ExtractedEntityType, -} from '../domain/marketSignal' -import type { MarketSignal } from '../domain/marketSignal' -import { AssetType, SignalType, SensitivityLevel, FreshnessStatus } from '../domain/enums' - -export const MOCK_MARKET_SIGNALS: MarketSignal[] = [ - { - id: 'sig-001', - title: 'UBS plant 200 neue Stellen – Büroflächenbedarf Zürich Innenstadt', - summary: 'UBS AG plant laut Medienbericht die Einstellung von 200 Spezialisten im Bereich Digital Banking bis Ende 2025. Standort ist primär Zürich. Der abgeleitete Flächenbedarf beträgt ca. 2\'500 m² zusätzlicher Bürofläche.', - sourceCategory: MarketSignalSourceCategory.COMPANY_NEWS, - sourceLabel: 'Neue Zürcher Zeitung', - sourceUrl: 'https://www.nzz.ch/', - detectedAt: '2025-05-10T09:23:00Z', - location: 'Zürich, ZH', - affectedAssetTypes: [AssetType.OFFICE], - signalType: SignalType.EXPANSION, - rawEvidenceSummary: '"UBS plant signifikante Aufstockung der digitalen Teams" – NZZ 10.05.2025', - extractedEntities: [ - { type: ExtractedEntityType.COMPANY, value: 'UBS AG', confidence: 0.98 }, - { type: ExtractedEntityType.LOCATION, value: 'Zürich', confidence: 0.95 }, - { type: ExtractedEntityType.DATE, value: 'Ende 2025', confidence: 0.87 }, - ], - evidence: [ - { - id: 'ev-001-1', signalId: 'sig-001', - evidenceType: EvidenceType.TEXT_EXCERPT, - content: '"UBS plant signifikante Aufstockung der digitalen Teams in Zürich" – NZZ, 10.05.2025', - sourceUrl: 'https://www.nzz.ch/', - retrievedAt: '2025-05-10T09:23:00Z', confidence: 0.92, - }, - ], - sourceReliabilityScore: 0.85, - confidenceScore: 0.72, - sensitivityLevel: SensitivityLevel.INTERNAL, - freshnessStatus: FreshnessStatus.FRESH, - processingStatus: SignalProcessingStatus.NEEDS_REVIEW, - analystNotes: 'Flächenbedarf abgeleitet, nicht direkt kommuniziert. Bestätigung durch Quellenanfrage empfohlen.', - createdAt: '2025-05-10T10:00:00Z', - updatedAt: '2025-05-11T08:00:00Z', - }, - { - id: 'sig-002', - title: 'Baugesuch für neues Gewerbepark-Areal in Basel-Nord', - summary: 'Im Handelsregister Basel-Stadt wurde ein Baugesuch für ein gemischt genutztes Gewerbeareal mit ca. 8\'000 m² Nutzfläche eingereicht. Projekt umfasst Büro, Logistik und Retail. Baubeginn geplant für Q1 2026.', - sourceCategory: MarketSignalSourceCategory.BUILDING_PERMIT_REGISTER, - sourceLabel: 'Bau- und Gastgewerbeinspektorat Basel-Stadt', - detectedAt: '2025-05-08T14:00:00Z', - location: 'Basel, BS', - affectedAssetTypes: [AssetType.OFFICE, AssetType.LOGISTICS, AssetType.RETAIL], - signalType: SignalType.CONSTRUCTION_PROJECT, - rawEvidenceSummary: 'Baugesuch Nr. BS-2025-0312 – gemischte Gewerbefläche, 8\'000 m², Basel-Nord', - extractedEntities: [ - { type: ExtractedEntityType.LOCATION, value: 'Basel-Nord', confidence: 0.97 }, - { type: ExtractedEntityType.ASSET, value: 'Gewerbeareal 8\'000 m²', confidence: 0.88 }, - { type: ExtractedEntityType.DATE, value: 'Q1 2026', confidence: 0.83 }, - ], - evidence: [ - { - id: 'ev-002-1', signalId: 'sig-002', - evidenceType: EvidenceType.URL_REFERENCE, - content: 'Baugesuch Nr. BS-2025-0312, Bau- und Gastgewerbeinspektorat Basel-Stadt', - retrievedAt: '2025-05-08T14:00:00Z', confidence: 0.95, - }, - ], - sourceReliabilityScore: 0.92, - confidenceScore: 0.88, - sensitivityLevel: SensitivityLevel.PUBLIC, - freshnessStatus: FreshnessStatus.FRESH, - processingStatus: SignalProcessingStatus.APPROVED_AS_SIGNAL, - analystNotes: 'Öffentliches Register. Hohe Verlässlichkeit. Eigentümerstruktur noch nicht ermittelt.', - createdAt: '2025-05-08T15:00:00Z', - updatedAt: '2025-05-12T09:30:00Z', - }, - { - id: 'sig-003', - title: 'Crypto-Unternehmen in Zug verdreifacht Belegschaft – Expansionsbedarf', - summary: 'Ein in Zug ansässiges Blockchain-Unternehmen hat laut LinkedIn-Auswertung innerhalb von 6 Monaten 80 neue Stellen ausgeschrieben. Bisherige Bürofläche reicht nicht mehr aus. Umzug oder Erweiterung erwartet.', - sourceCategory: MarketSignalSourceCategory.JOB_GROWTH_SIGNAL, - sourceLabel: 'LinkedIn Hiring Data', - detectedAt: '2025-05-09T11:30:00Z', - location: 'Zug, ZG', - affectedAssetTypes: [AssetType.OFFICE], - signalType: SignalType.EXPANSION, - rawEvidenceSummary: '80 offene Stellen in 6 Monaten (LinkedIn), Mitarbeiterzahl +210% YoY', - extractedEntities: [ - { type: ExtractedEntityType.LOCATION, value: 'Zug', confidence: 0.91 }, - { type: ExtractedEntityType.DATE, value: '6 Monate', confidence: 0.78 }, - ], - evidence: [ - { - id: 'ev-003-1', signalId: 'sig-003', - evidenceType: EvidenceType.TEXT_EXCERPT, - content: '80 offene Stellen in den letzten 6 Monaten. Mitarbeiterwachstum +210% gegenüber Vorjahr.', - retrievedAt: '2025-05-09T11:30:00Z', confidence: 0.78, - }, - ], - sourceReliabilityScore: 0.67, - confidenceScore: 0.61, - sensitivityLevel: SensitivityLevel.PUBLIC, - freshnessStatus: FreshnessStatus.FRESH, - processingStatus: SignalProcessingStatus.ENRICHED, - analystNotes: 'Unternehmensname nicht öffentlich kommuniziert. Weitere Verifizierung über HR-Netzwerke empfohlen.', - createdAt: '2025-05-09T12:00:00Z', - updatedAt: '2025-05-11T14:00:00Z', - }, - { - id: 'sig-004', - title: 'Vertragslaufdaten: Grossmieter Hardturmstrasse 201 läuft 03/2026 aus', - summary: 'Laut internen Vertragsdaten läuft der Mietvertrag eines Grossmieters an der Hardturmstrasse 201, Zürich, im März 2026 aus. Kontakt für Verlängerung wurde noch nicht aufgenommen. Mietfläche: ca. 3\'200 m².', - sourceCategory: MarketSignalSourceCategory.LEASE_EXPIRY_DATA, - sourceLabel: 'Internes ERP – Vertragsverwaltung', - detectedAt: '2025-05-05T08:00:00Z', - location: 'Zürich-West, ZH', - affectedAssetTypes: [AssetType.OFFICE], - signalType: SignalType.POSSIBLE_MOVE_OUT, - rawEvidenceSummary: 'Vertrag ID V-2019-0481, Ablauf 31.03.2026, keine Verlängerungsoption', - extractedEntities: [ - { type: ExtractedEntityType.LOCATION, value: 'Hardturmstrasse 201, Zürich', confidence: 0.99 }, - { type: ExtractedEntityType.DATE, value: '31.03.2026', confidence: 0.99 }, - { type: ExtractedEntityType.ASSET, value: '3\'200 m² Bürofläche', confidence: 0.97 }, - ], - evidence: [ - { - id: 'ev-004-1', signalId: 'sig-004', - evidenceType: EvidenceType.DOCUMENT, - content: 'Mietvertrag V-2019-0481, Laufzeit bis 31.03.2026, keine Option auf Verlängerung.', - retrievedAt: '2025-05-05T08:00:00Z', confidence: 0.99, - }, - ], - sourceReliabilityScore: 0.99, - confidenceScore: 0.97, - sensitivityLevel: SensitivityLevel.CONFIDENTIAL, - freshnessStatus: FreshnessStatus.FRESH, - processingStatus: SignalProcessingStatus.NEEDS_REVIEW, - analystNotes: 'VERTRAULICH – Nur intern. Mieteridentität darf nicht in Demand Feed erscheinen. Property Manager informieren.', - createdAt: '2025-05-05T08:30:00Z', - updatedAt: '2025-05-10T10:00:00Z', - }, - { - id: 'sig-005', - title: 'Sitzverlegung: Logistikunternehmen wechselt von Bern nach Zürich-Flughafen', - summary: 'Laut Handelsregistermutation hat ein mittelgrosses Logistikunternehmen seinen Hauptsitz von Bern nach Kloten verlegt. Aktiver Suchprozess nach Lagerfläche am Flughafen Zürich wahrscheinlich.', - sourceCategory: MarketSignalSourceCategory.COMMERCIAL_REGISTER, - sourceLabel: 'Handelsregister Schweiz – Zefix', - sourceUrl: 'https://www.zefix.ch/', - detectedAt: '2025-05-07T16:20:00Z', - location: 'Kloten, ZH', - affectedAssetTypes: [AssetType.LOGISTICS], - signalType: SignalType.POSSIBLE_MOVE_OUT, - rawEvidenceSummary: 'HR-Mutation: Sitzverlegung von 3014 Bern nach 8302 Kloten, eingetragen 07.05.2025', - extractedEntities: [ - { type: ExtractedEntityType.LOCATION, value: 'Kloten', confidence: 0.97 }, - { type: ExtractedEntityType.LOCATION, value: 'Bern', confidence: 0.97 }, - { type: ExtractedEntityType.DATE, value: '07.05.2025', confidence: 0.99 }, - ], - evidence: [ - { - id: 'ev-005-1', signalId: 'sig-005', - evidenceType: EvidenceType.URL_REFERENCE, - content: 'Handelsregistermutation: Sitzverlegung von 3014 Bern nach 8302 Kloten, eingetragen 07.05.2025', - sourceUrl: 'https://www.zefix.ch/', - retrievedAt: '2025-05-07T16:20:00Z', confidence: 0.97, - }, - ], - sourceReliabilityScore: 0.91, - confidenceScore: 0.63, - sensitivityLevel: SensitivityLevel.PUBLIC, - freshnessStatus: FreshnessStatus.FRESH, - processingStatus: SignalProcessingStatus.NORMALIZED, - analystNotes: 'Sitzverlegung bestätigt. Flächenbedarf am Zielort noch zu validieren.', - createdAt: '2025-05-07T17:00:00Z', - updatedAt: '2025-05-08T09:00:00Z', - }, - { - id: 'sig-006', - title: 'Neue Ausschreibung: 800 m² Lager-/Logistik in Winterthur', - summary: 'Auf Immoscout24 wurde eine neue Ausschreibung für 800 m² Lagerfläche in Winterthur publiziert. Suche aktiv, Einzug ab September 2025 gewünscht.', - sourceCategory: MarketSignalSourceCategory.PUBLIC_LISTING_PLATFORM, - sourceLabel: 'Immoscout24', - sourceUrl: 'https://www.immoscout24.ch/', - detectedAt: '2025-05-12T08:15:00Z', - location: 'Winterthur, ZH', - affectedAssetTypes: [AssetType.LOGISTICS], - signalType: SignalType.EXPANSION, - rawEvidenceSummary: 'Inseratstitel: "800m² Lager gesucht – Winterthur sofort" – Immoscout24, 12.05.2025', - extractedEntities: [ - { type: ExtractedEntityType.LOCATION, value: 'Winterthur', confidence: 0.99 }, - { type: ExtractedEntityType.ASSET, value: '800 m² Lagerfläche', confidence: 0.96 }, - { type: ExtractedEntityType.DATE, value: 'September 2025', confidence: 0.88 }, - ], - evidence: [ - { - id: 'ev-006-1', signalId: 'sig-006', - evidenceType: EvidenceType.URL_REFERENCE, - content: '"800m² Lager gesucht – Winterthur sofort" – Immoscout24, 12.05.2025', - sourceUrl: 'https://www.immoscout24.ch/', - retrievedAt: '2025-05-12T08:15:00Z', confidence: 0.96, - }, - ], - sourceReliabilityScore: 0.78, - confidenceScore: 0.83, - sensitivityLevel: SensitivityLevel.PUBLIC, - freshnessStatus: FreshnessStatus.FRESH, - processingStatus: SignalProcessingStatus.DETECTED, - analystNotes: '', - createdAt: '2025-05-12T08:30:00Z', - updatedAt: '2025-05-12T08:30:00Z', - }, - { - id: 'sig-007', - title: 'Neue S-Bahn-Station Schlieren 2027 – Aufwertung für Gewerbeflächen', - summary: 'Die SBB hat die neue Haltestelle Schlieren-West für 2027 bestätigt. Das umliegende Gewerbegebiet wird deutlich aufgewertet. Frühzeitige Positionierung im Büro- und Produktionssegment empfohlen.', - sourceCategory: MarketSignalSourceCategory.INFRASTRUCTURE_PROJECT, - sourceLabel: 'SBB Medienmitteilung', - sourceUrl: 'https://www.sbb.ch/de/medien.html', - detectedAt: '2025-05-06T10:00:00Z', - location: 'Schlieren, ZH', - affectedAssetTypes: [AssetType.OFFICE, AssetType.PRODUCTION], - signalType: SignalType.PROJECT_DEVELOPMENT, - rawEvidenceSummary: 'SBB bestätigt neue Haltestelle Schlieren-West, Inbetriebnahme Dezember 2027', - extractedEntities: [ - { type: ExtractedEntityType.LOCATION, value: 'Schlieren-West', confidence: 0.98 }, - { type: ExtractedEntityType.DATE, value: 'Dezember 2027', confidence: 0.95 }, - ], - evidence: [ - { - id: 'ev-007-1', signalId: 'sig-007', - evidenceType: EvidenceType.URL_REFERENCE, - content: 'SBB Medienmitteilung: Neue Haltestelle Schlieren-West, Inbetriebnahme Dezember 2027', - sourceUrl: 'https://www.sbb.ch/de/medien.html', - retrievedAt: '2025-05-06T10:00:00Z', confidence: 0.95, - }, - ], - sourceReliabilityScore: 0.96, - confidenceScore: 0.79, - sensitivityLevel: SensitivityLevel.PUBLIC, - freshnessStatus: FreshnessStatus.FRESH, - processingStatus: SignalProcessingStatus.ENRICHED, - analystNotes: 'Infrastrukturverbesserung. Kein direktes Verfügbarkeitssignal, aber relevanter Standortfaktor.', - createdAt: '2025-05-06T11:00:00Z', - updatedAt: '2025-05-10T15:00:00Z', - }, - { - id: 'sig-008', - title: 'Analyst-Beobachtung: Leerstehende Etage Tour de Berne, Lausanne', - summary: 'Eigene Marktbeobachtung: Die 4. Etage im Bürogebäude "Tour de Berne" in Lausanne steht seit mindestens 3 Monaten leer. Kein offizielles Inserat gefunden. Mögliche Vermarktung oder Leerstand durch Eigentümer.', - sourceCategory: MarketSignalSourceCategory.MANUAL_ANALYST_SIGNAL, - sourceLabel: 'Eigene Beobachtung – Marktanalyse Mai 2025', - detectedAt: '2025-05-03T14:30:00Z', - location: 'Lausanne, VD', - affectedAssetTypes: [AssetType.OFFICE], - signalType: SignalType.POSSIBLE_MOVE_OUT, - rawEvidenceSummary: 'Begehung 03.05.2025: 4. OG leer, keine Beschilderung, kein Inserat gefunden.', - extractedEntities: [ - { type: ExtractedEntityType.LOCATION, value: 'Tour de Berne, Lausanne', confidence: 0.94 }, - { type: ExtractedEntityType.ASSET, value: '4. Obergeschoss', confidence: 0.92 }, - ], - evidence: [ - { - id: 'ev-008-1', signalId: 'sig-008', - evidenceType: EvidenceType.ANALYST_NOTE, - content: 'Vor-Ort-Begehung am 03.05.2025: 4. OG im Tour de Berne leer stehend, keine aktive Vermarktung erkennbar.', - retrievedAt: '2025-05-03T14:30:00Z', confidence: 0.82, - }, - ], - sourceReliabilityScore: 0.72, - confidenceScore: 0.68, - sensitivityLevel: SensitivityLevel.INTERNAL, - freshnessStatus: FreshnessStatus.STALE, - processingStatus: SignalProcessingStatus.CONVERTED_TO_FUTURE_AVAILABILITY, - possibleFutureSignalId: 'fs-lausanne-001', - analystNotes: 'Zu Future Availability überführt. Eigentümer Kontaktaufnahme ausstehend.', - createdAt: '2025-05-03T15:00:00Z', - updatedAt: '2025-05-13T11:00:00Z', - }, -] diff --git a/.claude/worktrees/agent-a82a3716/src/mock-data/matches.ts b/.claude/worktrees/agent-a82a3716/src/mock-data/matches.ts deleted file mode 100644 index 35c8a85..0000000 --- a/.claude/worktrees/agent-a82a3716/src/mock-data/matches.ts +++ /dev/null @@ -1,1557 +0,0 @@ -import { MatchStrength, MatchStatus, RiskLevel } from '../domain/enums' -import type { Match } from '../domain/match' - -export const mockMatches: Match[] = [ - - // ─────────────────────────────────────────────────────────────────────────── - // need-001 · Innovatech AG · OFFICE Zürich · 600–1000m² · max CHF 45/m² - // ─────────────────────────────────────────────────────────────────────────── - - { - id: 'match-001', - propertyId: 'prop-001', - needId: 'need-001', - matchScore: 88, - matchStrength: MatchStrength.STRONG, - status: MatchStatus.APPROVED, - 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, - status: MatchStatus.PENDING_REVIEW, - 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.', - 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-005', - propertyId: 'prop-007', - needId: 'need-001', - matchScore: 86, - matchStrength: MatchStrength.STRONG, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 89, softFactorScore: 83, confidenceModifier: 0.98, dataQualityModifier: 0.94, totalScore: 86 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.20, score: 92, contribution: 18.4, explanation: '720m² im Zielkorridor (600–1000m²)' }, - { criterion: 'Standort', weight: 0.20, score: 90, contribution: 18, explanation: 'Zürich Oerlikon – bevorzugte Lage' }, - { criterion: 'Budget', weight: 0.15, score: 90, contribution: 13.5, explanation: 'CHF 36/m² deutlich unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Prestige', weight: 0.10, score: 72, contribution: 7.2, explanation: 'Prestige-Score 72 leicht unter Mindestanforderung 70 – ok' }, - ], - tradeoffs: [], - explainabilitySummary: 'Starker Match in Zürich Oerlikon. Fläche, Budget und ÖV-Anbindung erfüllen alle Hauptkriterien. Bewertung analog zu prop-001.', - confidenceLevel: 0.91, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: [], - organizationId: 'org-wincasa', - createdAt: '2025-05-10T08:20:00Z', - updatedAt: '2025-05-10T08:20:00Z', - }, - - { - id: 'match-006', - propertyId: 'prop-013', - needId: 'need-001', - matchScore: 72, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.SHORTLISTED, - scoreBreakdown: { hardMatchScore: 80, softFactorScore: 68, confidenceModifier: 0.96, dataQualityModifier: 0.92, totalScore: 72 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.20, score: 88, contribution: 17.6, explanation: 'Zürich Altstetten – akzeptable Lage' }, - { criterion: 'Budget', weight: 0.15, score: 95, contribution: 14.25, explanation: 'CHF 45/m² exakt im Budget-Limit' }, - ], - negativeFactors: [ - { criterion: 'Nutzungsart', weight: 0.15, score: 60, contribution: 9, explanation: 'MIXED-Fläche, Bedarf ist OFFICE – Teileignung' }, - { criterion: 'Fläche', weight: 0.20, score: 65, contribution: 13, explanation: '1300m² überschreitet Maximum von 1000m² deutlich' }, - ], - tradeoffs: [ - { criterion: 'Nutzungsart', concern: 'Gemischt genutzte Fläche – Büroanteil nicht spezifiziert', severity: 'MEDIUM', mitigation: 'Aufteilung klären, evtl. nur Büroanteil mieten' }, - ], - explainabilitySummary: 'Moderater Match – Lage und Budget stimmen, aber die Fläche ist zu gross und der gemischte Nutzungstyp passt nur teilweise.', - confidenceLevel: 0.82, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: ['Büroanteil der Gesamtfläche unklar'], - organizationId: 'org-wincasa', - createdAt: '2025-05-10T08:25:00Z', - updatedAt: '2025-05-10T08:25:00Z', - }, - - { - id: 'match-007', - propertyId: 'prop-005', - needId: 'need-001', - matchScore: 59, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 82, softFactorScore: 66, confidenceModifier: 0.55, dataQualityModifier: 0.38, totalScore: 59 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.20, score: 90, contribution: 18, explanation: 'Zürich Technopark – bevorzugte Lage' }, - { criterion: 'Fläche', weight: 0.20, score: 88, contribution: 17.6, explanation: '600m² an unterem Rand des Zielkorridors' }, - ], - negativeFactors: [ - { criterion: 'Konfidenz', weight: 0.15, score: 28, contribution: 4.2, explanation: 'Probabilistisches Signal – kein bestätigtes Objekt' }, - { criterion: 'Datenqualität', weight: 0.10, score: 22, contribution: 2.2, explanation: 'Kritische Felder fehlen, Daten nicht verifiziert' }, - ], - tradeoffs: [ - { criterion: 'Verfügbarkeit', concern: 'Signal nur 55% Wahrscheinlichkeit – keine Garantie', severity: 'HIGH', mitigation: 'Für Monitoring-Watchlist geeignet' }, - ], - explainabilitySummary: 'Lage und Fläche passen gut, aber die hohe Unsicherheit durch das probabilistische Signal zieht den Score deutlich nach unten.', - confidenceLevel: 0.48, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Probabilistisches Signal', 'Kritische Daten fehlen'], - organizationId: 'org-wincasa', - createdAt: '2025-05-10T08:30:00Z', - updatedAt: '2025-05-10T08:30:00Z', - }, - - { - id: 'match-008', - propertyId: 'prop-023', - needId: 'need-001', - matchScore: 62, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 85, softFactorScore: 69, confidenceModifier: 0.54, dataQualityModifier: 0.36, totalScore: 62 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.20, score: 85, contribution: 17, explanation: 'Zürich-Nord / Seebach – bevorzugter Kanton ZH' }, - { criterion: 'Budget', weight: 0.15, score: 92, contribution: 13.8, explanation: 'CHF 38/m² unter Maximum von CHF 45/m²' }, - ], - negativeFactors: [ - { criterion: 'Konfidenz', weight: 0.15, score: 32, contribution: 4.8, explanation: '54% Signalwahrscheinlichkeit – unbestätigt' }, - { criterion: 'Datenqualität', weight: 0.10, score: 20, contribution: 2, explanation: 'Probabilistisches Signal, Felder fehlen' }, - ], - tradeoffs: [ - { criterion: 'Verfügbarkeit', concern: 'Auszug von aktuellem Mieter nicht bestätigt', severity: 'HIGH', mitigation: 'Als Frühindikator beobachten' }, - ], - explainabilitySummary: 'Gute Lage in Zürich mit passendem Budget, aber Future-Signal mit mittlerer Konfidenz. Empfehlung: Beobachten.', - confidenceLevel: 0.46, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Auszug des Mieters nicht bestätigt', 'Daten unvollständig'], - organizationId: 'org-wincasa', - createdAt: '2025-05-10T08:35:00Z', - updatedAt: '2025-05-10T08:35:00Z', - }, - - { - id: 'match-009', - propertyId: 'prop-015', - needId: 'need-001', - matchScore: 51, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 65, softFactorScore: 53, confidenceModifier: 0.70, dataQualityModifier: 0.60, totalScore: 51 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.20, score: 88, contribution: 17.6, explanation: '650m² im Zielkorridor' }, - { criterion: 'Budget', weight: 0.15, score: 90, contribution: 13.5, explanation: 'CHF 38/m² unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.20, score: 35, contribution: 7, explanation: 'Luzern liegt ausserhalb bevorzugter Lage Zürich' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Luzern ist nicht in Zürich – komplett andere Stadt und Kanton', severity: 'HIGH' }, - ], - explainabilitySummary: 'Fläche und Budget passen, aber die Lage in Luzern ist nicht mit dem Bedarf Zürich kompatibel. Nur als letzte Option geeignet.', - confidenceLevel: 0.55, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Standort ausserhalb bevorzugter Region'], - organizationId: 'org-wincasa', - createdAt: '2025-05-10T08:40:00Z', - updatedAt: '2025-05-10T08:40:00Z', - }, - - // ─────────────────────────────────────────────────────────────────────────── - // need-002 · Schweizer Logistik GmbH · LOGISTICS Basel · 1500–4000m² · max CHF 18/m² - // ─────────────────────────────────────────────────────────────────────────── - - { - id: 'match-003', - propertyId: 'prop-002', - needId: 'need-002', - matchScore: 91, - matchStrength: MatchStrength.STRONG, - status: MatchStatus.APPROVED, - 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²' }, - ], - 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, - status: MatchStatus.SHORTLISTED, - 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' }, - ], - explainabilitySummary: 'Schwacher Match aufgrund hoher Unsicherheit. Das Signal ist interessant als Frühindikator, aber nicht als aktive Option geeignet.', - confidenceLevel: 0.38, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Probabilistisches Signal ohne Bestätigung', 'Mietpreis geschätzt'], - 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', - }, - - { - id: 'match-010', - propertyId: 'prop-014', - needId: 'need-002', - matchScore: 87, - matchStrength: MatchStrength.STRONG, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 90, softFactorScore: 84, confidenceModifier: 0.98, dataQualityModifier: 0.94, totalScore: 87 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.25, score: 95, contribution: 23.75, explanation: '3100m² optimal im Zielkorridor (1500–4000m²)' }, - { criterion: 'Standort', weight: 0.20, score: 82, contribution: 16.4, explanation: 'Pratteln BL – Kanton BL, direkte Nähe zu Basel' }, - { criterion: 'Budget', weight: 0.20, score: 88, contribution: 17.6, explanation: 'CHF 15/m² unter Maximum von CHF 18/m²' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.05, score: 65, contribution: 3.25, explanation: 'Nicht direkt Basel-Stadt, aber akzeptable Region' }, - ], - tradeoffs: [], - explainabilitySummary: 'Sehr starker Match. Logistikfläche in Pratteln erfüllt alle Hauptkriterien. Verfügbarkeit sofort, Daten vollständig.', - confidenceLevel: 0.93, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: [], - organizationId: 'org-wincasa', - createdAt: '2025-05-10T09:00:00Z', - updatedAt: '2025-05-10T09:00:00Z', - }, - - { - id: 'match-011', - propertyId: 'prop-016', - needId: 'need-002', - matchScore: 76, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.APPROVED, - scoreBreakdown: { hardMatchScore: 90, softFactorScore: 74, confidenceModifier: 0.69, dataQualityModifier: 0.58, totalScore: 76 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.25, score: 92, contribution: 23, explanation: '2200m² im Zielkorridor' }, - { criterion: 'Standort', weight: 0.20, score: 78, contribution: 15.6, explanation: 'Muttenz BL – gleicher Kanton, gute Lage' }, - { criterion: 'Budget', weight: 0.20, score: 85, contribution: 17, explanation: 'CHF 16/m² innerhalb Budget' }, - ], - negativeFactors: [ - { criterion: 'Datenqualität', weight: 0.10, score: 58, contribution: 5.8, explanation: 'Externe Quelle – Hallenhöhe nicht bestätigt' }, - ], - tradeoffs: [ - { criterion: 'Datenqualität', concern: 'Hallenhöhe und Andienung aus externer Quelle – Vor-Ort-Check empfohlen', severity: 'MEDIUM' }, - ], - explainabilitySummary: 'Solider Marktinserat-Match in Muttenz. Fläche und Budget stimmen, Datenverifikation ausstehend.', - confidenceLevel: 0.72, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Hallenhöhe nicht bestätigt', 'Externe Quelle'], - organizationId: 'org-wincasa', - createdAt: '2025-05-10T09:05:00Z', - updatedAt: '2025-05-10T09:05:00Z', - }, - - { - id: 'match-012', - propertyId: 'prop-009', - needId: 'need-002', - matchScore: 55, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 74, softFactorScore: 60, confidenceModifier: 0.98, dataQualityModifier: 0.95, totalScore: 55 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.25, score: 85, contribution: 21.25, explanation: '1800m² im Zielkorridor' }, - { criterion: 'Budget', weight: 0.20, score: 90, contribution: 18, explanation: 'CHF 13/m² weit unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.20, score: 35, contribution: 7, explanation: 'Winterthur liegt ausserhalb Präferenz Basel-Region' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Winterthur statt Basel – komplett andere Region, kein A2-Anschluss', severity: 'HIGH' }, - ], - explainabilitySummary: 'Fläche und Preis passen hervorragend, aber Winterthur ist nicht die gewünschte Logistik-Region Basel. Nur als Alternativoption.', - confidenceLevel: 0.82, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: ['Standort ausserhalb Präferenzregion'], - organizationId: 'org-wincasa', - createdAt: '2025-05-10T09:10:00Z', - updatedAt: '2025-05-10T09:10:00Z', - }, - - { - id: 'match-013', - propertyId: 'prop-024', - needId: 'need-002', - matchScore: 63, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.SHORTLISTED, - scoreBreakdown: { hardMatchScore: 86, softFactorScore: 70, confidenceModifier: 0.52, dataQualityModifier: 0.35, totalScore: 63 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.20, score: 95, contribution: 19, explanation: 'Basel Hafen – exakte Präferenzlage' }, - { criterion: 'Fläche', weight: 0.25, score: 88, contribution: 22, explanation: '2600m² im Zielkorridor' }, - ], - negativeFactors: [ - { criterion: 'Konfidenz', weight: 0.15, score: 32, contribution: 4.8, explanation: 'Neubau-Signal – Fertigstellung Q2 2026' }, - { criterion: 'Datenqualität', weight: 0.10, score: 25, contribution: 2.5, explanation: 'Mieterkonditionen noch nicht bekannt' }, - ], - tradeoffs: [ - { criterion: 'Timing', concern: 'Fertigstellung Juli 2026 – möglicherweise zu spät', severity: 'MEDIUM', mitigation: 'Voranmietung prüfen' }, - ], - explainabilitySummary: 'Sehr gute Lage direkt am Basler Hafen. Als Neubau mit hoher Bauwahrscheinlichkeit auf Shortlist geeignet.', - confidenceLevel: 0.44, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Fertigstellung abhängig von Baufortschritt'], - organizationId: 'org-wincasa', - createdAt: '2025-05-10T09:15:00Z', - updatedAt: '2025-05-10T09:15:00Z', - }, - - { - id: 'match-014', - propertyId: 'prop-029', - needId: 'need-002', - matchScore: 56, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 79, softFactorScore: 63, confidenceModifier: 0.46, dataQualityModifier: 0.31, totalScore: 56 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.25, score: 90, contribution: 22.5, explanation: '3500m² im Zielkorridor' }, - { criterion: 'Budget', weight: 0.20, score: 92, contribution: 18.4, explanation: 'CHF 13/m² deutlich unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Konfidenz', weight: 0.15, score: 28, contribution: 4.2, explanation: 'Nur 50% Wahrscheinlichkeit, kein bestätigter Auszug' }, - { criterion: 'Standort', weight: 0.20, score: 60, contribution: 12, explanation: 'Frenkendorf BL – gleicher Kanton, aber nicht Basel-Stadt' }, - ], - tradeoffs: [ - { criterion: 'Verfügbarkeit', concern: 'Probabilistisches Signal – Auszug unbestätigt', severity: 'HIGH' }, - ], - explainabilitySummary: 'Gleicher Kanton, gutes Preis-Leistungs-Verhältnis, aber Future-Signal mit niedriger Konfidenz. Als Watchlist-Kandidat geeignet.', - confidenceLevel: 0.40, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Auszug unbestätigt', 'Konditionen unbekannt'], - organizationId: 'org-wincasa', - createdAt: '2025-05-10T09:20:00Z', - updatedAt: '2025-05-10T09:20:00Z', - }, - - // ─────────────────────────────────────────────────────────────────────────── - // need-003 · Pharma Holding AG · OFFICE Basel · 500–800m² · max CHF 40/m² - // ─────────────────────────────────────────────────────────────────────────── - - { - id: 'match-015', - propertyId: 'prop-008', - needId: 'need-003', - matchScore: 85, - matchStrength: MatchStrength.STRONG, - status: MatchStatus.APPROVED, - scoreBreakdown: { hardMatchScore: 88, softFactorScore: 82, confidenceModifier: 0.97, dataQualityModifier: 0.93, totalScore: 85 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.25, score: 95, contribution: 23.75, explanation: 'Basel Dreispitz – exakte Präferenzlage' }, - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 32/m² deutlich unter Maximum CHF 40/m²' }, - { criterion: 'Prestige', weight: 0.12, score: 70, contribution: 8.4, explanation: 'Prestige 70 erfüllt Mindestanforderung' }, - ], - negativeFactors: [ - { criterion: 'Fläche', weight: 0.20, score: 72, contribution: 14.4, explanation: '900m² liegt über Maximum 800m² – etwas zu gross' }, - ], - tradeoffs: [ - { criterion: 'Fläche', concern: '900m² leicht über Maximum, möglicherweise Untereinheit verhandelbar', severity: 'LOW' }, - ], - explainabilitySummary: 'Starker Match in Basel. Prestige, Budget und Lage stimmen. Fläche minimal über Wunschgrösse, aber verhandelbar.', - confidenceLevel: 0.90, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: [], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T08:00:00Z', - updatedAt: '2025-05-11T08:00:00Z', - }, - - { - id: 'match-016', - propertyId: 'prop-007', - needId: 'need-003', - matchScore: 52, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 72, softFactorScore: 60, confidenceModifier: 0.98, dataQualityModifier: 0.94, totalScore: 52 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.20, score: 90, contribution: 18, explanation: '720m² im Zielkorridor' }, - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 36/m² unter Maximum CHF 40/m²' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.25, score: 35, contribution: 8.75, explanation: 'Zürich statt Basel – andere Stadt, anderer Kanton' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Zürich ist nicht im Präferenzgebiet Basel/Allschwil', severity: 'HIGH' }, - ], - explainabilitySummary: 'Fläche und Preis stimmen, aber Zürich entspricht nicht dem Standortbedarf Basel. Nur wenn Flexibilität vorhanden.', - confidenceLevel: 0.86, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: ['Standortanforderung nicht erfüllt'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T08:05:00Z', - updatedAt: '2025-05-11T08:05:00Z', - }, - - { - id: 'match-017', - propertyId: 'prop-018', - needId: 'need-003', - matchScore: 44, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 58, softFactorScore: 46, confidenceModifier: 0.68, dataQualityModifier: 0.57, totalScore: 44 }, - positiveFactors: [ - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 31/m² deutlich unter Maximum' }, - { criterion: 'Fläche', weight: 0.20, score: 88, contribution: 17.6, explanation: '780m² nahe am Zielkorridor' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.25, score: 35, contribution: 8.75, explanation: 'Bern liegt ausserhalb Präferenz Basel' }, - { criterion: 'Datenqualität', weight: 0.10, score: 42, contribution: 4.2, explanation: 'Externe Quelle, Renovierungsstand unklar' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Bern ist nicht Basel – keine Nähe zur Pharma-Industrie-Achse', severity: 'HIGH' }, - ], - explainabilitySummary: 'Schwacher Match aufgrund Standort-Mismatch. Bern ist nicht im Präferenzgebiet Basel. Budget und Fläche ok, aber Lage kritisch.', - confidenceLevel: 0.54, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Standort ausserhalb Präferenzregion', 'Externe Quelle'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T08:10:00Z', - updatedAt: '2025-05-11T08:10:00Z', - }, - - { - id: 'match-018', - propertyId: 'prop-005', - needId: 'need-003', - matchScore: 38, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.REJECTED, - scoreBreakdown: { hardMatchScore: 61, softFactorScore: 45, confidenceModifier: 0.55, dataQualityModifier: 0.38, totalScore: 38 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.20, score: 88, contribution: 17.6, explanation: '600m² im Zielkorridor' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.25, score: 35, contribution: 8.75, explanation: 'Zürich statt Basel – 80km Entfernung' }, - { criterion: 'Budget', weight: 0.20, score: 70, contribution: 14, explanation: 'CHF 42/m² liegt 5% über Budget-Maximum' }, - { criterion: 'Konfidenz', weight: 0.15, score: 25, contribution: 3.75, explanation: 'Future-Signal, kein bestätigtes Objekt' }, - ], - tradeoffs: [ - { criterion: 'Standort & Verfügbarkeit', concern: 'Falscher Standort + Future-Signal macht diesen Match nicht empfehlenswert', severity: 'HIGH' }, - ], - explainabilitySummary: 'Zu viele kritische Mängel: falscher Standort, Budget leicht über Maximum, und probabilistisches Signal. Abgelehnt.', - confidenceLevel: 0.36, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Falscher Standort', 'Future-Signal', 'Budget knapp'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T08:15:00Z', - updatedAt: '2025-05-11T08:15:00Z', - }, - - { - id: 'match-019', - propertyId: 'prop-015', - needId: 'need-003', - matchScore: 42, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 56, softFactorScore: 44, confidenceModifier: 0.70, dataQualityModifier: 0.60, totalScore: 42 }, - positiveFactors: [ - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 38/m² unter Maximum CHF 40/m²' }, - { criterion: 'Fläche', weight: 0.20, score: 88, contribution: 17.6, explanation: '650m² im Zielkorridor' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.25, score: 35, contribution: 8.75, explanation: 'Luzern liegt ausserhalb Region Basel' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Luzern ist nicht im Präferenzgebiet, 100km von Basel', severity: 'HIGH' }, - ], - explainabilitySummary: 'Budget und Fläche passen, jedoch ist Luzern nicht mit dem Bedarf Raum Basel vereinbar.', - confidenceLevel: 0.56, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Standort ausserhalb Präferenz'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T08:20:00Z', - updatedAt: '2025-05-11T08:20:00Z', - }, - - // ─────────────────────────────────────────────────────────────────────────── - // need-004 · Retailer Zürich AG · RETAIL Zürich · 200–500m² · max CHF 100/m² - // ─────────────────────────────────────────────────────────────────────────── - - { - id: 'match-020', - propertyId: 'prop-010', - needId: 'need-004', - matchScore: 91, - matchStrength: MatchStrength.STRONG, - status: MatchStatus.APPROVED, - scoreBreakdown: { hardMatchScore: 94, softFactorScore: 88, confidenceModifier: 0.99, dataQualityModifier: 0.96, totalScore: 91 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.35, score: 98, contribution: 34.3, explanation: 'Zürich Löwenplatz – exakte Innenstadtlage, Topstandort' }, - { criterion: 'Fläche', weight: 0.15, score: 90, contribution: 13.5, explanation: '285m² im Zielkorridor' }, - { criterion: 'Prestige', weight: 0.15, score: 95, contribution: 14.25, explanation: 'Prestige 95 – Topstandort erfüllt Anforderung' }, - ], - negativeFactors: [ - { criterion: 'Budget', weight: 0.15, score: 80, contribution: 12, explanation: 'CHF 88/m² unter Maximum CHF 100/m², leicht over avg' }, - ], - tradeoffs: [], - explainabilitySummary: 'Exzellenter Match. Zürich Löwenplatz ist ein Erstklasstandort mit höchster Passantenfrequenz. Alle Kriterien erfüllt.', - confidenceLevel: 0.96, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: [], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T09:00:00Z', - updatedAt: '2025-05-11T09:00:00Z', - }, - - { - id: 'match-021', - propertyId: 'prop-017', - needId: 'need-004', - matchScore: 79, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.SHORTLISTED, - scoreBreakdown: { hardMatchScore: 93, softFactorScore: 77, confidenceModifier: 0.71, dataQualityModifier: 0.61, totalScore: 79 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.35, score: 94, contribution: 32.9, explanation: 'Zürich Löwenstrasse – Innenstadtlage' }, - { criterion: 'Budget', weight: 0.15, score: 95, contribution: 14.25, explanation: 'CHF 95/m² unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Datenqualität', weight: 0.10, score: 61, contribution: 6.1, explanation: 'Mietpreis aus externer Quelle, nicht bestätigt' }, - ], - tradeoffs: [ - { criterion: 'Datenqualität', concern: 'Mietpreis und Vertragsdauer nicht bestätigt', severity: 'MEDIUM', mitigation: 'Verifizierung beim Vermieter empfohlen' }, - ], - explainabilitySummary: 'Sehr gute Lage in Zürich Innenstadt. Datenverifikation ausstehend, aber Standort und Preis stimmen.', - confidenceLevel: 0.72, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Mietpreis nicht final bestätigt'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T09:05:00Z', - updatedAt: '2025-05-11T09:05:00Z', - }, - - { - id: 'match-022', - propertyId: 'prop-025', - needId: 'need-004', - matchScore: 62, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 85, softFactorScore: 69, confidenceModifier: 0.50, dataQualityModifier: 0.33, totalScore: 62 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.35, score: 90, contribution: 31.5, explanation: 'Zürich Niederdorf – Innenstadtlage, gute Passantenfrequenz' }, - { criterion: 'Budget', weight: 0.15, score: 95, contribution: 14.25, explanation: 'CHF 92/m² unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Konfidenz', weight: 0.15, score: 30, contribution: 4.5, explanation: '55% Signalwahrscheinlichkeit – unbestätigt' }, - { criterion: 'Datenqualität', weight: 0.10, score: 20, contribution: 2, explanation: 'Future-Signal, kritische Felder fehlen' }, - ], - tradeoffs: [ - { criterion: 'Verfügbarkeit', concern: 'Probabilistisches Signal – Auszug noch nicht bestätigt', severity: 'HIGH' }, - ], - explainabilitySummary: 'Gute Lage im Niederdorf, aber Future-Signal mit mittlerer Konfidenz. Empfehlung: Beobachten und bei Bestätigung priorisieren.', - confidenceLevel: 0.42, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Auszug unbestätigt', 'Future-Signal'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T09:10:00Z', - updatedAt: '2025-05-11T09:10:00Z', - }, - - { - id: 'match-023', - propertyId: 'prop-003', - needId: 'need-004', - matchScore: 44, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 58, softFactorScore: 46, confidenceModifier: 0.71, dataQualityModifier: 0.62, totalScore: 44 }, - positiveFactors: [ - { criterion: 'Budget', weight: 0.15, score: 95, contribution: 14.25, explanation: 'CHF 95/m² unter Maximum CHF 100/m²' }, - { criterion: 'Prestige', weight: 0.15, score: 92, contribution: 13.8, explanation: 'Berner Bahnhofstrasse – Top-Prestige' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.35, score: 35, contribution: 12.25, explanation: 'Bern statt Zürich Innenstadt – andere Stadt, anderer Kanton' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Bern ist nicht Zürich – Laufkundschaft aus anderem Einzugsgebiet', severity: 'HIGH' }, - ], - explainabilitySummary: 'Gutes Retail-Objekt in Bern, aber der Bedarf ist explizit Zürich Innenstadt. Standort ist ausschlaggebend für Ablehnung.', - confidenceLevel: 0.60, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Standort nicht im Zielgebiet'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T09:15:00Z', - updatedAt: '2025-05-11T09:15:00Z', - }, - - { - id: 'match-024', - propertyId: 'prop-021', - needId: 'need-004', - matchScore: 33, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.REJECTED, - scoreBreakdown: { hardMatchScore: 47, softFactorScore: 35, confidenceModifier: 0.70, dataQualityModifier: 0.59, totalScore: 33 }, - positiveFactors: [ - { criterion: 'Prestige', weight: 0.15, score: 94, contribution: 14.1, explanation: 'Rue du Rhône Genf – sehr hoher Prestige-Score' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.35, score: 35, contribution: 12.25, explanation: 'Genf ist nicht Zürich – 280km Entfernung' }, - { criterion: 'Budget', weight: 0.15, score: 68, contribution: 10.2, explanation: 'CHF 112/m² liegt 12% über Maximum CHF 100/m²' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Genf ist eine komplett andere Stadt als Zürich', severity: 'HIGH' }, - { criterion: 'Budget', concern: 'Mietpreis 12% über Maximum', severity: 'MEDIUM' }, - ], - explainabilitySummary: 'Abgelehnt. Genf und Zürich sind nicht kompatibel – weder geografisch noch bezüglich des angestrebten Kundenkreises. Budget ebenfalls überschritten.', - confidenceLevel: 0.52, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Falscher Standort', 'Budget überschritten'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T09:20:00Z', - updatedAt: '2025-05-11T09:20:00Z', - }, - - // ─────────────────────────────────────────────────────────────────────────── - // need-005 · TechStart GmbH · OFFICE Zug/Zürich · 300–700m² · max CHF 48/m² - // ─────────────────────────────────────────────────────────────────────────── - - { - id: 'match-025', - propertyId: 'prop-012', - needId: 'need-005', - matchScore: 91, - matchStrength: MatchStrength.STRONG, - status: MatchStatus.APPROVED, - scoreBreakdown: { hardMatchScore: 94, softFactorScore: 88, confidenceModifier: 0.97, dataQualityModifier: 0.93, totalScore: 91 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.25, score: 100, contribution: 25, explanation: 'Zug Zentrum – exakt bevorzugte Lage' }, - { criterion: 'Fläche', weight: 0.20, score: 92, contribution: 18.4, explanation: '550m² im Zielkorridor (300–700m²)' }, - { criterion: 'Budget', weight: 0.20, score: 90, contribution: 18, explanation: 'CHF 42/m² klar unter Maximum CHF 48/m²' }, - ], - negativeFactors: [], - tradeoffs: [], - explainabilitySummary: 'Exzellenter Match. Zug Zentrum trifft exakt den Standortwunsch. Budget, Fläche und Ausbaugrad erfüllen alle Anforderungen.', - confidenceLevel: 0.92, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: [], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T10:00:00Z', - updatedAt: '2025-05-11T10:00:00Z', - }, - - { - id: 'match-026', - propertyId: 'prop-007', - needId: 'need-005', - matchScore: 83, - matchStrength: MatchStrength.STRONG, - status: MatchStatus.SHORTLISTED, - scoreBreakdown: { hardMatchScore: 86, softFactorScore: 80, confidenceModifier: 0.98, dataQualityModifier: 0.94, totalScore: 83 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.25, score: 88, contribution: 22, explanation: 'Zürich Oerlikon – in bevorzugter Stadt Zürich' }, - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 36/m² deutlich unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Fläche', weight: 0.20, score: 72, contribution: 14.4, explanation: '720m² überschreitet Maximum von 700m² leicht' }, - ], - tradeoffs: [ - { criterion: 'Fläche', concern: 'Leicht über Flächenmaximum – ggf. Untereinheit verhandelbar', severity: 'LOW' }, - ], - explainabilitySummary: 'Sehr guter Match in Zürich Oerlikon. Lage und Budget stimmen, Fläche minimal über Maximum.', - confidenceLevel: 0.89, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: [], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T10:05:00Z', - updatedAt: '2025-05-11T10:05:00Z', - }, - - { - id: 'match-027', - propertyId: 'prop-020', - needId: 'need-005', - matchScore: 74, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 88, softFactorScore: 72, confidenceModifier: 0.70, dataQualityModifier: 0.60, totalScore: 74 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.25, score: 100, contribution: 25, explanation: 'Zug – exakte Präferenzstadt' }, - { criterion: 'Budget', weight: 0.20, score: 90, contribution: 18, explanation: 'CHF 44/m² unter Maximum CHF 48/m²' }, - ], - negativeFactors: [ - { criterion: 'Datenqualität', weight: 0.10, score: 60, contribution: 6, explanation: 'Externe Quelle, Vertragsdauer fehlt' }, - { criterion: 'Fläche', weight: 0.20, score: 68, contribution: 13.6, explanation: '820m² deutlich über Maximum 700m²' }, - ], - tradeoffs: [ - { criterion: 'Fläche', concern: 'Fläche 17% über Maximum – Untereinheit prüfen', severity: 'MEDIUM' }, - ], - explainabilitySummary: 'Guter Standort Zug, Budget akzeptabel. Fläche überschreitet Maximum aber Datenqualität eingeschränkt.', - confidenceLevel: 0.68, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Fläche zu gross', 'Externe Quelle'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T10:10:00Z', - updatedAt: '2025-05-11T10:10:00Z', - }, - - { - id: 'match-028', - propertyId: 'prop-001', - needId: 'need-005', - matchScore: 78, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.APPROVED, - scoreBreakdown: { hardMatchScore: 81, softFactorScore: 75, confidenceModifier: 0.97, dataQualityModifier: 0.92, totalScore: 78 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.25, score: 88, contribution: 22, explanation: 'Zürich-West – bevorzugte Stadt Zürich' }, - { criterion: 'Budget', weight: 0.20, score: 90, contribution: 18, explanation: 'CHF 38/m² unter Maximum CHF 48/m²' }, - ], - negativeFactors: [ - { criterion: 'Fläche', weight: 0.20, score: 62, contribution: 12.4, explanation: '850m² überschreitet Maximum 700m² deutlich' }, - ], - tradeoffs: [ - { criterion: 'Fläche', concern: '850m² sind 21% über Maximum – Untereinheit oder Kompromiss nötig', severity: 'MEDIUM', mitigation: '650m²-Untereinheit im selben Gebäude verfügbar' }, - ], - explainabilitySummary: 'Zürich-West passt gut. Fläche zu gross, aber Untereinheit verfügbar. Budget und Ausbaugrad sehr gut.', - confidenceLevel: 0.88, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: ['Fläche über Maximum'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T10:15:00Z', - updatedAt: '2025-05-11T10:15:00Z', - }, - - { - id: 'match-029', - propertyId: 'prop-026', - needId: 'need-005', - matchScore: 64, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 87, softFactorScore: 71, confidenceModifier: 0.52, dataQualityModifier: 0.34, totalScore: 64 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.25, score: 100, contribution: 25, explanation: 'Zug – exakter Standortwunsch' }, - { criterion: 'Budget', weight: 0.20, score: 90, contribution: 18, explanation: 'CHF 43/m² unter Maximum CHF 48/m²' }, - ], - negativeFactors: [ - { criterion: 'Konfidenz', weight: 0.15, score: 30, contribution: 4.5, explanation: '52% Signalwahrscheinlichkeit – Future-Signal' }, - { criterion: 'Datenqualität', weight: 0.10, score: 22, contribution: 2.2, explanation: 'Kritische Felder fehlen' }, - ], - tradeoffs: [ - { criterion: 'Verfügbarkeit', concern: 'Expansion-Signal – Fläche noch nicht auf dem Markt', severity: 'HIGH' }, - ], - explainabilitySummary: 'Perfekter Standort Zug, Budget stimmt. Als Future-Signal mit 52% Konfidenz – Watchlist-Kandidat.', - confidenceLevel: 0.44, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Future-Signal', 'Daten unvollständig'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T10:20:00Z', - updatedAt: '2025-05-11T10:20:00Z', - }, - - { - id: 'match-030', - propertyId: 'prop-022', - needId: 'need-005', - matchScore: 43, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 57, softFactorScore: 45, confidenceModifier: 0.69, dataQualityModifier: 0.58, totalScore: 43 }, - positiveFactors: [ - { criterion: 'Budget', weight: 0.20, score: 100, contribution: 20, explanation: 'CHF 28/m² deutlich unter Maximum' }, - { criterion: 'Fläche', weight: 0.20, score: 88, contribution: 17.6, explanation: '700m² exakt an der Obergrenze' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.25, score: 35, contribution: 8.75, explanation: 'St. Gallen ist nicht Zug oder Zürich – andere Kantone' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'St. Gallen liegt 80km von Zug entfernt – nicht im Präferenzgebiet', severity: 'HIGH' }, - ], - explainabilitySummary: 'Budget hervorragend, aber Standort St. Gallen passt nicht zu Zug/Zürich. Nur als äusserste Alternative.', - confidenceLevel: 0.55, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Standort ausserhalb Präferenz'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T10:25:00Z', - updatedAt: '2025-05-11T10:25:00Z', - }, - - // ─────────────────────────────────────────────────────────────────────────── - // need-006 · Lager & Spedition AG · LOGISTICS Winterthur · 1200–3000m² · max CHF 16/m² - // ─────────────────────────────────────────────────────────────────────────── - - { - id: 'match-031', - propertyId: 'prop-009', - needId: 'need-006', - matchScore: 93, - matchStrength: MatchStrength.STRONG, - status: MatchStatus.APPROVED, - scoreBreakdown: { hardMatchScore: 96, softFactorScore: 90, confidenceModifier: 0.98, dataQualityModifier: 0.95, totalScore: 93 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.25, score: 100, contribution: 25, explanation: 'Winterthur Töss – exakte Präferenzlage' }, - { criterion: 'Fläche', weight: 0.30, score: 92, contribution: 27.6, explanation: '1800m² im Zielkorridor (1200–3000m²)' }, - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 13/m² unter Maximum CHF 16/m²' }, - ], - negativeFactors: [], - tradeoffs: [], - explainabilitySummary: 'Ausgezeichneter Match. Winterthur Töss trifft exakt den Standortwunsch. Alle Logistik-Kriterien erfüllt, hervorragende Datenqualität.', - confidenceLevel: 0.96, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: [], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T11:00:00Z', - updatedAt: '2025-05-11T11:00:00Z', - }, - - { - id: 'match-032', - propertyId: 'prop-002', - needId: 'need-006', - matchScore: 56, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 76, softFactorScore: 62, confidenceModifier: 0.99, dataQualityModifier: 0.96, totalScore: 56 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.30, score: 90, contribution: 27, explanation: '2400m² im Zielkorridor' }, - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 14/m² unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.25, score: 35, contribution: 8.75, explanation: 'Basel liegt ausserhalb Winterthur-Region, andere Stadt' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Basel ist nicht im Präferenzgebiet Winterthur', severity: 'HIGH' }, - ], - explainabilitySummary: 'Sehr gute Logistikfläche in Basel, aber falscher Standort. Nur wenn Winterthur nicht verfügbar.', - confidenceLevel: 0.82, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: ['Standort ausserhalb Region'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T11:05:00Z', - updatedAt: '2025-05-11T11:05:00Z', - }, - - { - id: 'match-033', - propertyId: 'prop-014', - needId: 'need-006', - matchScore: 52, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 72, softFactorScore: 58, confidenceModifier: 0.98, dataQualityModifier: 0.94, totalScore: 52 }, - positiveFactors: [ - { criterion: 'Budget', weight: 0.20, score: 92, contribution: 18.4, explanation: 'CHF 15/m² unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.25, score: 35, contribution: 8.75, explanation: 'Pratteln BL ist nicht Winterthur – andere Stadt, anderer Kanton' }, - { criterion: 'Fläche', weight: 0.30, score: 68, contribution: 20.4, explanation: '3100m² überschreitet Maximum 3000m² leicht' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Pratteln liegt im Raum Basel, nicht im Raum Winterthur', severity: 'HIGH' }, - ], - explainabilitySummary: 'Falscher Standort für Winterthur-Bedarf. Nur als letzter Ausweg wenn Präferenz verhandelbar.', - confidenceLevel: 0.85, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: ['Standort nicht kompatibel'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T11:10:00Z', - updatedAt: '2025-05-11T11:10:00Z', - }, - - { - id: 'match-034', - propertyId: 'prop-016', - needId: 'need-006', - matchScore: 50, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 64, softFactorScore: 50, confidenceModifier: 0.69, dataQualityModifier: 0.58, totalScore: 50 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.30, score: 88, contribution: 26.4, explanation: '2200m² im Zielkorridor' }, - { criterion: 'Budget', weight: 0.20, score: 85, contribution: 17, explanation: 'CHF 16/m² exakt im Maximum' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.25, score: 35, contribution: 8.75, explanation: 'Muttenz liegt im Raum Basel, nicht Winterthur' }, - { criterion: 'Datenqualität', weight: 0.10, score: 58, contribution: 5.8, explanation: 'Hallenhöhe nicht verifiziert' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Muttenz BL ist nicht die Präferenz Winterthur', severity: 'HIGH' }, - ], - explainabilitySummary: 'Fläche und Budget stimmen, aber Muttenz ist 70km von Winterthur entfernt. Nicht kompatibel.', - confidenceLevel: 0.58, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Standort nicht kompatibel', 'Hallenhöhe unklar'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T11:15:00Z', - updatedAt: '2025-05-11T11:15:00Z', - }, - - { - id: 'match-035', - propertyId: 'prop-029', - needId: 'need-006', - matchScore: 43, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 66, softFactorScore: 53, confidenceModifier: 0.46, dataQualityModifier: 0.31, totalScore: 43 }, - positiveFactors: [ - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 13/m² unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.25, score: 35, contribution: 8.75, explanation: 'Frenkendorf BL nicht im Präferenzgebiet Winterthur' }, - { criterion: 'Konfidenz', weight: 0.15, score: 28, contribution: 4.2, explanation: 'Future-Signal 50% Wahrscheinlichkeit' }, - { criterion: 'Fläche', weight: 0.30, score: 65, contribution: 19.5, explanation: '3500m² über Maximum 3000m²' }, - ], - tradeoffs: [ - { criterion: 'Kombination', concern: 'Falscher Standort + Future-Signal + zu grosse Fläche', severity: 'HIGH' }, - ], - explainabilitySummary: 'Drei kritische Faktoren: falscher Standort, zu grosse Fläche und Future-Signal. Keine Empfehlung.', - confidenceLevel: 0.36, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Standort', 'Zu grosse Fläche', 'Future-Signal'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T11:20:00Z', - updatedAt: '2025-05-11T11:20:00Z', - }, - - // ─────────────────────────────────────────────────────────────────────────── - // need-007 · Creative Studios AG · MIXED Zürich/Bern · 800–1500m² · max CHF 55/m² - // ─────────────────────────────────────────────────────────────────────────── - - { - id: 'match-036', - propertyId: 'prop-013', - needId: 'need-007', - matchScore: 89, - matchStrength: MatchStrength.STRONG, - status: MatchStatus.APPROVED, - scoreBreakdown: { hardMatchScore: 92, softFactorScore: 86, confidenceModifier: 0.96, dataQualityModifier: 0.92, totalScore: 89 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.20, score: 95, contribution: 19, explanation: 'Zürich Altstetten – bevorzugte Lage Zürich' }, - { criterion: 'Nutzungsart', weight: 0.20, score: 100, contribution: 20, explanation: 'MIXED – exakt passend für Creative Studios' }, - { criterion: 'Fläche', weight: 0.20, score: 90, contribution: 18, explanation: '1300m² im Zielkorridor (800–1500m²)' }, - { criterion: 'Budget', weight: 0.15, score: 95, contribution: 14.25, explanation: 'CHF 45/m² klar unter Maximum CHF 55/m²' }, - ], - negativeFactors: [], - tradeoffs: [], - explainabilitySummary: 'Hervorragender Match. Gemischte Fläche in Zürich Altstetten erfüllt alle Anforderungen für Kreativagentur.', - confidenceLevel: 0.90, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: [], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T12:00:00Z', - updatedAt: '2025-05-11T12:00:00Z', - }, - - { - id: 'match-037', - propertyId: 'prop-004', - needId: 'need-007', - matchScore: 76, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.SHORTLISTED, - scoreBreakdown: { hardMatchScore: 90, softFactorScore: 74, confidenceModifier: 0.68, dataQualityModifier: 0.55, totalScore: 76 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.20, score: 92, contribution: 18.4, explanation: 'Zürich Kreis 4 – gute Kreativlage in Zürich' }, - { criterion: 'Nutzungsart', weight: 0.20, score: 100, contribution: 20, explanation: 'MIXED-Objekt passt exakt' }, - { criterion: 'Budget', weight: 0.15, score: 88, contribution: 13.2, explanation: 'CHF 52/m² unter Maximum CHF 55/m²' }, - ], - negativeFactors: [ - { criterion: 'Datenqualität', weight: 0.10, score: 55, contribution: 5.5, explanation: 'Externe Quelle – Daten unvollständig' }, - ], - tradeoffs: [ - { criterion: 'Datenqualität', concern: 'Vertragsdauer und Nebenkosten nicht bekannt', severity: 'MEDIUM' }, - ], - explainabilitySummary: 'Guter Match in Zürich. MIXED-Objekt passt, Budget stimmt. Datenverifikation nötig.', - confidenceLevel: 0.64, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Externe Quelle', 'Fehlende Felder'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T12:05:00Z', - updatedAt: '2025-05-11T12:05:00Z', - }, - - { - id: 'match-038', - propertyId: 'prop-007', - needId: 'need-007', - matchScore: 62, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 76, softFactorScore: 65, confidenceModifier: 0.98, dataQualityModifier: 0.94, totalScore: 62 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.20, score: 90, contribution: 18, explanation: 'Zürich Oerlikon – bevorzugte Lage' }, - { criterion: 'Budget', weight: 0.15, score: 100, contribution: 15, explanation: 'CHF 36/m² deutlich unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Nutzungsart', weight: 0.20, score: 60, contribution: 12, explanation: 'OFFICE-Fläche, Bedarf ist MIXED – Teileignung' }, - { criterion: 'Fläche', weight: 0.20, score: 60, contribution: 12, explanation: '720m² unter Minimum von 800m²' }, - ], - tradeoffs: [ - { criterion: 'Nutzungsart', concern: 'Bürofläche erlaubt ggf. keine gemischte Nutzung', severity: 'MEDIUM', mitigation: 'Nutzungsbewilligung prüfen' }, - ], - explainabilitySummary: 'Guter Standort und Budget, aber OFFICE-Fläche ist nicht ideal für MIXED-Bedarf und Fläche unter Minimum.', - confidenceLevel: 0.80, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: ['Nutzungsart nicht optimal', 'Fläche unter Minimum'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T12:10:00Z', - updatedAt: '2025-05-11T12:10:00Z', - }, - - // ─────────────────────────────────────────────────────────────────────────── - // need-008 · Berner Produzenten GmbH · PRODUCTION Bern · 2000–4000m² · max CHF 14/m² - // ─────────────────────────────────────────────────────────────────────────── - - { - id: 'match-039', - propertyId: 'prop-011', - needId: 'need-008', - matchScore: 92, - matchStrength: MatchStrength.STRONG, - status: MatchStatus.APPROVED, - scoreBreakdown: { hardMatchScore: 95, softFactorScore: 89, confidenceModifier: 0.98, dataQualityModifier: 0.95, totalScore: 92 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.20, score: 100, contribution: 20, explanation: 'Bern Brünnen – exakte Präferenzlage' }, - { criterion: 'Fläche', weight: 0.30, score: 95, contribution: 28.5, explanation: '2800m² optimal im Zielkorridor (2000–4000m²)' }, - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 12/m² unter Maximum CHF 14/m²' }, - ], - negativeFactors: [], - tradeoffs: [], - explainabilitySummary: 'Ausgezeichneter Match. Produktionshalle Bern Brünnen erfüllt alle Anforderungen. Verfügbar sofort, vollständige Daten.', - confidenceLevel: 0.94, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: [], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T13:00:00Z', - updatedAt: '2025-05-11T13:00:00Z', - }, - - { - id: 'match-040', - propertyId: 'prop-027', - needId: 'need-008', - matchScore: 65, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.SHORTLISTED, - scoreBreakdown: { hardMatchScore: 88, softFactorScore: 72, confidenceModifier: 0.48, dataQualityModifier: 0.32, totalScore: 65 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.20, score: 82, contribution: 16.4, explanation: 'Münchenbuchsee – Kanton Bern, nahe Präferenzlage' }, - { criterion: 'Fläche', weight: 0.30, score: 88, contribution: 26.4, explanation: '2200m² im Zielkorridor' }, - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 12/m² unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Konfidenz', weight: 0.15, score: 28, contribution: 4.2, explanation: 'Future-Signal 52% Wahrscheinlichkeit' }, - { criterion: 'Datenqualität', weight: 0.10, score: 20, contribution: 2, explanation: 'Kritische Felder fehlen' }, - ], - tradeoffs: [ - { criterion: 'Verfügbarkeit', concern: 'Auszug noch unbestätigt – Verfügbarkeit August 2026 unsicher', severity: 'HIGH', mitigation: 'Monitoring empfohlen, in 3 Monaten neu evaluieren' }, - ], - explainabilitySummary: 'Guter Standort im Kanton Bern, Fläche und Budget passen. Aber als Future-Signal mit mittlerer Konfidenz auf Watchlist.', - confidenceLevel: 0.40, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Future-Signal', 'Auszug unbestätigt'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T13:05:00Z', - updatedAt: '2025-05-11T13:05:00Z', - }, - - { - id: 'match-041', - propertyId: 'prop-006', - needId: 'need-008', - matchScore: 44, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 67, softFactorScore: 51, confidenceModifier: 0.48, dataQualityModifier: 0.30, totalScore: 44 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.30, score: 85, contribution: 25.5, explanation: '3200m² im Zielkorridor' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.20, score: 60, contribution: 12, explanation: 'Reinach BL – anderer Kanton, nicht Bern' }, - { criterion: 'Konfidenz', weight: 0.15, score: 25, contribution: 3.75, explanation: 'Future-Signal 48% Wahrscheinlichkeit' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Reinach BL liegt im Kanton BL, nicht im Kanton BE', severity: 'MEDIUM' }, - { criterion: 'Verfügbarkeit', concern: 'Future-Signal – kein bestätigtes Objekt', severity: 'HIGH' }, - ], - explainabilitySummary: 'Fläche passt, aber Standort (Kanton BL statt BE) und Future-Signal sind Risikofaktoren. Schwacher Match.', - confidenceLevel: 0.36, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Anderer Kanton', 'Future-Signal'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T13:10:00Z', - updatedAt: '2025-05-11T13:10:00Z', - }, - - { - id: 'match-042', - propertyId: 'prop-019', - needId: 'need-008', - matchScore: 47, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 61, softFactorScore: 47, confidenceModifier: 0.68, dataQualityModifier: 0.56, totalScore: 47 }, - positiveFactors: [ - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 13/m² unter Maximum CHF 14/m²' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.20, score: 35, contribution: 7, explanation: 'Basel ist nicht Bern – andere Stadt, anderer Kanton' }, - { criterion: 'Fläche', weight: 0.30, score: 62, contribution: 18.6, explanation: '1900m² leicht unter Minimum 2000m²' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Basel BS liegt 100km von Bern entfernt', severity: 'HIGH' }, - ], - explainabilitySummary: 'Falscher Standort und Fläche leicht unter Minimum. Budget stimmt, reicht aber nicht aus. Schwacher Match.', - confidenceLevel: 0.54, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Falscher Standort', 'Fläche unter Minimum'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T13:15:00Z', - updatedAt: '2025-05-11T13:15:00Z', - }, - - // ─────────────────────────────────────────────────────────────────────────── - // need-009 · Geneva Commerce SA · RETAIL Genf · 150–400m² · max CHF 120/m² - // ─────────────────────────────────────────────────────────────────────────── - - { - id: 'match-043', - propertyId: 'prop-021', - needId: 'need-009', - matchScore: 83, - matchStrength: MatchStrength.STRONG, - status: MatchStatus.SHORTLISTED, - scoreBreakdown: { hardMatchScore: 97, softFactorScore: 81, confidenceModifier: 0.70, dataQualityModifier: 0.59, totalScore: 83 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.35, score: 100, contribution: 35, explanation: 'Genf Rue du Rhône – exakter Standortwunsch' }, - { criterion: 'Budget', weight: 0.15, score: 92, contribution: 13.8, explanation: 'CHF 112/m² unter Maximum CHF 120/m²' }, - { criterion: 'Prestige', weight: 0.15, score: 94, contribution: 14.1, explanation: 'Prestige 94 – erfüllt hohe Anforderung 88' }, - ], - negativeFactors: [ - { criterion: 'Datenqualität', weight: 0.10, score: 59, contribution: 5.9, explanation: 'Externe Quelle – Konditionen nicht bestätigt' }, - ], - tradeoffs: [ - { criterion: 'Datenqualität', concern: 'Mietpreis und Vertragsdauer aus externer Quelle', severity: 'MEDIUM', mitigation: 'Direktkontakt mit Anbieter empfohlen' }, - ], - explainabilitySummary: 'Bester verfügbarer Match für Genf. Standort, Prestige und Budget stimmen. Datenverifikation ausstehend.', - confidenceLevel: 0.66, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Konditionen nicht bestätigt'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T14:00:00Z', - updatedAt: '2025-05-11T14:00:00Z', - }, - - { - id: 'match-044', - propertyId: 'prop-010', - needId: 'need-009', - matchScore: 45, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 59, softFactorScore: 46, confidenceModifier: 0.99, dataQualityModifier: 0.96, totalScore: 45 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.15, score: 90, contribution: 13.5, explanation: '285m² im Zielkorridor' }, - { criterion: 'Budget', weight: 0.15, score: 100, contribution: 15, explanation: 'CHF 88/m² deutlich unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.35, score: 35, contribution: 12.25, explanation: 'Zürich ist nicht Genf – andere Stadt, 280km' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Zürich und Genf haben komplett verschiedene Kundenpotenziale', severity: 'HIGH' }, - ], - explainabilitySummary: 'Sehr gute Qualität in Zürich, aber falscher Standort für einen Genfer Retailer. Nicht geeignet.', - confidenceLevel: 0.86, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: ['Falscher Standort'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T14:05:00Z', - updatedAt: '2025-05-11T14:05:00Z', - }, - - { - id: 'match-045', - propertyId: 'prop-017', - needId: 'need-009', - matchScore: 38, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 52, softFactorScore: 40, confidenceModifier: 0.71, dataQualityModifier: 0.61, totalScore: 38 }, - positiveFactors: [ - { criterion: 'Budget', weight: 0.15, score: 100, contribution: 15, explanation: 'CHF 95/m² unter Maximum CHF 120/m²' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.35, score: 35, contribution: 12.25, explanation: 'Zürich statt Genf – komplett andere Stadt' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Zürich ist geografisch und kulturell nicht der gesuchte Genfer Markt', severity: 'HIGH' }, - ], - explainabilitySummary: 'Budget und Qualität ok, aber Zürich ist kein Ersatz für Genf. Standort nicht erfüllt.', - confidenceLevel: 0.62, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Falscher Standort'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T14:10:00Z', - updatedAt: '2025-05-11T14:10:00Z', - }, - - // ─────────────────────────────────────────────────────────────────────────── - // need-010 · St.Galler Büros AG · OFFICE St.Gallen · 400–800m² · max CHF 35/m² - // ─────────────────────────────────────────────────────────────────────────── - - { - id: 'match-046', - propertyId: 'prop-022', - needId: 'need-010', - matchScore: 79, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.APPROVED, - scoreBreakdown: { hardMatchScore: 93, softFactorScore: 77, confidenceModifier: 0.69, dataQualityModifier: 0.58, totalScore: 79 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.28, score: 100, contribution: 28, explanation: 'St. Gallen Centrum – exakter Standortwunsch' }, - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 28/m² deutlich unter Maximum CHF 35/m²' }, - { criterion: 'Fläche', weight: 0.22, score: 90, contribution: 19.8, explanation: '700m² im Zielkorridor (400–800m²)' }, - ], - negativeFactors: [ - { criterion: 'Datenqualität', weight: 0.10, score: 58, contribution: 5.8, explanation: 'Externe Quelle – Ausbauqualität nicht bestätigt' }, - ], - tradeoffs: [ - { criterion: 'Datenqualität', concern: 'Ausbauqualität aus externer Quelle nicht verifiziert', severity: 'MEDIUM', mitigation: 'Vor-Ort-Besichtigung empfohlen' }, - ], - explainabilitySummary: 'Guter Match in St. Gallen Centrum. Alle Hauptkriterien erfüllt. Datenverifikation ausstehend.', - confidenceLevel: 0.72, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Externe Quelle', 'Ausbauqualität unklar'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T15:00:00Z', - updatedAt: '2025-05-11T15:00:00Z', - }, - - { - id: 'match-047', - propertyId: 'prop-030', - needId: 'need-010', - matchScore: 58, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 81, softFactorScore: 65, confidenceModifier: 0.55, dataQualityModifier: 0.36, totalScore: 58 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.28, score: 100, contribution: 28, explanation: 'St. Gallen Riethüsli – Präferenzstadt' }, - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 27/m² deutlich unter Maximum' }, - ], - negativeFactors: [ - { criterion: 'Konfidenz', weight: 0.15, score: 30, contribution: 4.5, explanation: 'Future-Signal 55% Wahrscheinlichkeit' }, - { criterion: 'Datenqualität', weight: 0.10, score: 22, contribution: 2.2, explanation: 'Kritische Felder fehlen' }, - ], - tradeoffs: [ - { criterion: 'Verfügbarkeit', concern: 'Future-Signal – kein bestätigter Auszug', severity: 'HIGH', mitigation: 'Auf Watchlist setzen' }, - ], - explainabilitySummary: 'Bester Standort St. Gallen, Budget sehr gut. Als Future-Signal mit mittlerer Konfidenz Watchlist-Kandidat.', - confidenceLevel: 0.44, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Future-Signal', 'Daten unvollständig'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T15:05:00Z', - updatedAt: '2025-05-11T15:05:00Z', - }, - - { - id: 'match-048', - propertyId: 'prop-007', - needId: 'need-010', - matchScore: 53, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 68, softFactorScore: 56, confidenceModifier: 0.98, dataQualityModifier: 0.94, totalScore: 53 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.22, score: 90, contribution: 19.8, explanation: '720m² im Zielkorridor' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.28, score: 35, contribution: 9.8, explanation: 'Zürich Oerlikon ist nicht St. Gallen – andere Stadt, anderer Kanton' }, - { criterion: 'Budget', weight: 0.20, score: 72, contribution: 14.4, explanation: 'CHF 36/m² liegt 3% über Maximum CHF 35/m²' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Zürich ist nicht im Präferenzgebiet Ostschweiz', severity: 'HIGH' }, - ], - explainabilitySummary: 'Falscher Standort und Budget leicht über Maximum. Zürich ist keine Alternative für St. Gallen.', - confidenceLevel: 0.86, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: ['Standort ausserhalb Ostschweiz', 'Budget knapp über Maximum'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T15:10:00Z', - updatedAt: '2025-05-11T15:10:00Z', - }, - - { - id: 'match-049', - propertyId: 'prop-015', - needId: 'need-010', - matchScore: 46, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 60, softFactorScore: 48, confidenceModifier: 0.70, dataQualityModifier: 0.60, totalScore: 46 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.22, score: 88, contribution: 19.36, explanation: '650m² im Zielkorridor' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.28, score: 35, contribution: 9.8, explanation: 'Luzern statt St. Gallen – andere Stadt, anderer Kanton' }, - { criterion: 'Budget', weight: 0.20, score: 74, contribution: 14.8, explanation: 'CHF 38/m² liegt 8% über Maximum CHF 35/m²' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Luzern ist nicht Ostschweiz / St. Gallen Region', severity: 'HIGH' }, - { criterion: 'Budget', concern: 'CHF 3/m² über Maximum', severity: 'MEDIUM' }, - ], - explainabilitySummary: 'Fläche ok, aber Luzern entspricht nicht der St. Gallen-Region und Budget leicht überschritten.', - confidenceLevel: 0.58, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Standort nicht kompatibel', 'Budget leicht über Maximum'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T15:15:00Z', - updatedAt: '2025-05-11T15:15:00Z', - }, - - { - id: 'match-050', - propertyId: 'prop-018', - needId: 'need-010', - matchScore: 38, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 52, softFactorScore: 40, confidenceModifier: 0.68, dataQualityModifier: 0.57, totalScore: 38 }, - positiveFactors: [ - { criterion: 'Budget', weight: 0.20, score: 100, contribution: 20, explanation: 'CHF 31/m² deutlich unter Maximum CHF 35/m²' }, - { criterion: 'Fläche', weight: 0.22, score: 85, contribution: 18.7, explanation: '780m² nahe Zielobergrenze' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.28, score: 35, contribution: 9.8, explanation: 'Bern ist nicht St. Gallen – weit ausserhalb Präferenzgebiet' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Bern liegt 180km von St. Gallen entfernt', severity: 'HIGH' }, - ], - explainabilitySummary: 'Budget sehr gut, aber Bern entspricht nicht dem Bedarf Ostschweiz. Kein sinnvoller Match.', - confidenceLevel: 0.54, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Falscher Standort'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T15:20:00Z', - updatedAt: '2025-05-11T15:20:00Z', - }, - - { - id: 'match-051', - propertyId: 'prop-008', - needId: 'need-010', - matchScore: 42, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 56, softFactorScore: 44, confidenceModifier: 0.97, dataQualityModifier: 0.93, totalScore: 42 }, - positiveFactors: [ - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 32/m² unter Maximum CHF 35/m²' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.28, score: 35, contribution: 9.8, explanation: 'Basel ist nicht St. Gallen – anderer Kanton' }, - { criterion: 'Fläche', weight: 0.22, score: 65, contribution: 14.3, explanation: '900m² überschreitet Maximum 800m² deutlich' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Basel liegt in Nordwestschweiz, nicht in Ostschweiz', severity: 'HIGH' }, - ], - explainabilitySummary: 'Gutes Portfolio-Objekt in Basel, aber falscher Standort und Fläche zu gross. Nicht geeignet.', - confidenceLevel: 0.84, - riskLevel: RiskLevel.LOW, - uncertaintyIndicators: ['Falscher Standort', 'Fläche zu gross'], - organizationId: 'org-wincasa', - createdAt: '2025-05-11T15:25:00Z', - updatedAt: '2025-05-11T15:25:00Z', - }, - - // ─────────────────────────────────────────────────────────────────────────── - // Cross-need additional matches - // ─────────────────────────────────────────────────────────────────────────── - - { - id: 'match-052', - propertyId: 'prop-020', - needId: 'need-003', - matchScore: 41, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 55, softFactorScore: 43, confidenceModifier: 0.70, dataQualityModifier: 0.60, totalScore: 41 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.20, score: 85, contribution: 17, explanation: '820m² nahe am Zielkorridor (500–800m²)' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.25, score: 35, contribution: 8.75, explanation: 'Zug ist nicht Basel – anderer Kanton' }, - { criterion: 'Budget', weight: 0.20, score: 72, contribution: 14.4, explanation: 'CHF 44/m² liegt 10% über Maximum CHF 40/m²' }, - ], - tradeoffs: [ - { criterion: 'Kombination', concern: 'Falscher Standort und Budget überschritten', severity: 'HIGH' }, - ], - explainabilitySummary: 'Falscher Standort (Zug statt Basel) und Budget überschritten. Kein Match empfehlenswert.', - confidenceLevel: 0.58, - riskLevel: RiskLevel.MEDIUM, - uncertaintyIndicators: ['Standort nicht kompatibel', 'Budget über Maximum'], - organizationId: 'org-wincasa', - createdAt: '2025-05-12T08:00:00Z', - updatedAt: '2025-05-12T08:00:00Z', - }, - - { - id: 'match-053', - propertyId: 'prop-023', - needId: 'need-005', - matchScore: 65, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 88, softFactorScore: 72, confidenceModifier: 0.54, dataQualityModifier: 0.36, totalScore: 65 }, - positiveFactors: [ - { criterion: 'Standort', weight: 0.25, score: 88, contribution: 22, explanation: 'Zürich-Nord – bevorzugte Stadt Zürich' }, - { criterion: 'Budget', weight: 0.20, score: 95, contribution: 19, explanation: 'CHF 38/m² unter Maximum CHF 48/m²' }, - ], - negativeFactors: [ - { criterion: 'Konfidenz', weight: 0.15, score: 30, contribution: 4.5, explanation: 'Future-Signal 54% Wahrscheinlichkeit' }, - { criterion: 'Fläche', weight: 0.20, score: 70, contribution: 14, explanation: '850m² leicht über Maximum 700m²' }, - ], - tradeoffs: [ - { criterion: 'Verfügbarkeit', concern: 'Future-Signal – kein bestätigtes Objekt', severity: 'HIGH' }, - ], - explainabilitySummary: 'Gute Zürich-Lage und Budget passend. Als Future-Signal mit mittlerer Konfidenz und leicht zu grosser Fläche beobachten.', - confidenceLevel: 0.46, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Future-Signal', 'Fläche leicht über Maximum'], - organizationId: 'org-wincasa', - createdAt: '2025-05-12T08:05:00Z', - updatedAt: '2025-05-12T08:05:00Z', - }, - - { - id: 'match-054', - propertyId: 'prop-028', - needId: 'need-010', - matchScore: 37, - matchStrength: MatchStrength.WEAK, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 60, softFactorScore: 50, confidenceModifier: 0.53, dataQualityModifier: 0.35, totalScore: 37 }, - positiveFactors: [ - { criterion: 'Fläche', weight: 0.22, score: 88, contribution: 19.36, explanation: '520m² im Zielkorridor' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.28, score: 35, contribution: 9.8, explanation: 'Genf ist nicht St. Gallen – 600km Entfernung' }, - { criterion: 'Budget', weight: 0.20, score: 72, contribution: 14.4, explanation: 'CHF 36/m² über Maximum CHF 35/m² knapp' }, - { criterion: 'Konfidenz', weight: 0.15, score: 28, contribution: 4.2, explanation: 'Future-Signal' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Genf ist nicht kompatibel mit St. Gallen Bedarf', severity: 'HIGH' }, - ], - explainabilitySummary: 'Falscher Standort, Budget knapp über Maximum und Future-Signal. Keine Empfehlung.', - confidenceLevel: 0.38, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Falscher Standort', 'Future-Signal'], - organizationId: 'org-wincasa', - createdAt: '2025-05-12T08:10:00Z', - updatedAt: '2025-05-12T08:10:00Z', - }, - - { - id: 'match-055', - propertyId: 'prop-026', - needId: 'need-001', - matchScore: 56, - matchStrength: MatchStrength.MODERATE, - status: MatchStatus.PENDING_REVIEW, - scoreBreakdown: { hardMatchScore: 79, softFactorScore: 63, confidenceModifier: 0.52, dataQualityModifier: 0.34, totalScore: 56 }, - positiveFactors: [ - { criterion: 'Budget', weight: 0.15, score: 92, contribution: 13.8, explanation: 'CHF 43/m² unter Maximum CHF 45/m²' }, - { criterion: 'Fläche', weight: 0.20, score: 85, contribution: 17, explanation: '580m² im Zielkorridor (600–1000m²), knapp unter Minimum' }, - ], - negativeFactors: [ - { criterion: 'Standort', weight: 0.20, score: 60, contribution: 12, explanation: 'Zug liegt nicht in Zürich, aber gleiche Wirtschaftsregion' }, - { criterion: 'Konfidenz', weight: 0.15, score: 30, contribution: 4.5, explanation: 'Future-Signal 52% Wahrscheinlichkeit' }, - ], - tradeoffs: [ - { criterion: 'Standort', concern: 'Zug ist nicht Zürich – Pendeldistanz 30 Min.', severity: 'MEDIUM' }, - { criterion: 'Verfügbarkeit', concern: 'Future-Signal – erst ab April 2026 möglicherweise verfügbar', severity: 'HIGH' }, - ], - explainabilitySummary: 'Budget und Fläche passen. Zug ist eine Alternativlage zur bevorzugten Lage Zürich. Future-Signal mit mittlerer Konfidenz.', - confidenceLevel: 0.42, - riskLevel: RiskLevel.HIGH, - uncertaintyIndicators: ['Standort ausserhalb Präferenz', 'Future-Signal'], - organizationId: 'org-wincasa', - createdAt: '2025-05-12T08:15:00Z', - updatedAt: '2025-05-12T08:15:00Z', - }, -] diff --git a/.claude/worktrees/agent-a82a3716/src/mock-data/needs.ts b/.claude/worktrees/agent-a82a3716/src/mock-data/needs.ts deleted file mode 100644 index f3a10dc..0000000 --- a/.claude/worktrees/agent-a82a3716/src/mock-data/needs.ts +++ /dev/null @@ -1,387 +0,0 @@ -import { AssetType } from '../domain/enums' -import type { Need } from '../domain/need' - -export const mockNeeds: Need[] = [ - // --- need-001: Innovatech AG — OFFICE Zürich --- - { - 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', - }, - - // --- need-002: Schweizer Logistik GmbH — LOGISTICS Basel --- - { - 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', - }, - - // --- need-003: Pharma Holding AG — OFFICE Basel --- - { - id: 'need-003', - companyName: 'Pharma Holding AG', - contactName: 'Ursula Schmid', - assetType: AssetType.OFFICE, - requiredArea: { min: 500, max: 800 }, - preferredLocations: ['Basel', 'Allschwil', 'Binningen', 'Dreispitz'], - excludedLocations: [], - budgetRange: { maxPerSqm: 40, maxMonthlyTotal: 32000, currency: 'CHF' }, - timing: { - earliestMoveIn: '2025-10-01', - latestMoveIn: '2026-04-01', - contractDurationMonths: 48, - flexibleTiming: true, - }, - mustCriteriaText: ['Repräsentative Lage', 'Ausbaugrad gehoben', 'Konferenzräume vorhanden'], - softFactors: { - minPrestige: 75, - minAccessibility: 75, - requireParking: true, - maxPublicTransportMinutes: 10, - }, - weightingProfile: { - area: 0.20, - location: 0.25, - budget: 0.20, - timing: 0.10, - prestige: 0.12, - accessibility: 0.08, - expansionPotential: 0.03, - flexibility: 0.02, - }, - confidenceInCriteria: 0.91, - extractedFromText: 'Suche repräsentative Büroflächen im Raum Basel/Allschwil, 500–800m², max. CHF 38/m², Bezug Q4 2025.', - organizationId: 'org-wincasa', - createdAt: '2025-04-20T09:00:00Z', - updatedAt: '2025-05-05T14:00:00Z', - }, - - // --- need-004: Retailer Zürich AG — RETAIL Zürich --- - { - id: 'need-004', - companyName: 'Retailer Zürich AG', - contactName: 'Marco Colombo', - assetType: AssetType.RETAIL, - requiredArea: { min: 200, max: 500 }, - preferredLocations: ['Zürich Innenstadt', 'Zürich Bahnhofstrasse', 'Zürich Niederdorf', 'Zürich City'], - excludedLocations: [], - budgetRange: { maxPerSqm: 100, maxMonthlyTotal: 50000, currency: 'CHF' }, - timing: { - earliestMoveIn: '2025-09-01', - latestMoveIn: '2026-03-01', - contractDurationMonths: 60, - flexibleTiming: false, - }, - mustCriteriaText: ['Laufkundschaft', 'Schaufensterfront', 'Erdgeschoss', 'Hohe Passantenfrequenz'], - softFactors: { - minPrestige: 85, - requireParking: false, - maxPublicTransportMinutes: 5, - }, - weightingProfile: { - area: 0.15, - location: 0.35, - budget: 0.15, - timing: 0.10, - prestige: 0.15, - accessibility: 0.05, - expansionPotential: 0.02, - flexibility: 0.03, - }, - confidenceInCriteria: 0.96, - extractedFromText: 'Exklusive Retailfläche in Zürich Innenstadt gesucht, 250–450m², Schaufenster, max. CHF 95/m².', - organizationId: 'org-wincasa', - createdAt: '2025-03-10T11:00:00Z', - updatedAt: '2025-04-22T08:00:00Z', - }, - - // --- need-005: TechStart GmbH — OFFICE Zug/Zürich --- - { - id: 'need-005', - companyName: 'TechStart GmbH', - contactName: 'Florian Keller', - assetType: AssetType.OFFICE, - requiredArea: { min: 300, max: 700 }, - preferredLocations: ['Zug', 'Zürich', 'Baar', 'Steinhausen'], - excludedLocations: [], - budgetRange: { maxPerSqm: 48, maxMonthlyTotal: 33000, currency: 'CHF' }, - timing: { - earliestMoveIn: '2025-10-01', - latestMoveIn: '2026-06-01', - contractDurationMonths: 36, - flexibleTiming: true, - }, - mustCriteriaText: ['Moderner Ausbau', 'Schnelles Internet', 'Fahrradabstellplätze'], - softFactors: { - minPrestige: 65, - minAccessibility: 75, - requireParking: false, - maxPublicTransportMinutes: 10, - }, - weightingProfile: { - area: 0.20, - location: 0.25, - budget: 0.20, - timing: 0.15, - prestige: 0.05, - accessibility: 0.10, - expansionPotential: 0.03, - flexibility: 0.02, - }, - confidenceInCriteria: 0.85, - extractedFromText: 'Junges Tech-Unternehmen sucht Büro in Zug oder Zürich, 350–600m², moderner Ausbau, max. CHF 45/m².', - organizationId: 'org-wincasa', - createdAt: '2025-04-28T13:00:00Z', - updatedAt: '2025-05-08T10:00:00Z', - }, - - // --- need-006: Lager & Spedition AG — LOGISTICS Winterthur --- - { - id: 'need-006', - companyName: 'Lager & Spedition AG', - contactName: 'Beat Zimmermann', - assetType: AssetType.LOGISTICS, - requiredArea: { min: 1200, max: 3000 }, - preferredLocations: ['Winterthur', 'Wülflingen', 'Oberwinterthur', 'Töss'], - budgetRange: { maxPerSqm: 16, currency: 'CHF' }, - timing: { - earliestMoveIn: '2025-11-01', - latestMoveIn: '2026-05-01', - contractDurationMonths: 60, - flexibleTiming: false, - }, - mustCriteriaText: ['Autobahn A1 < 10 Min', 'Ebenerdig', 'Lkw-Zufahrt', 'Sprinkleranlage'], - softFactors: { - requireParking: true, - }, - weightingProfile: { - area: 0.30, - location: 0.25, - budget: 0.20, - timing: 0.10, - prestige: 0.01, - accessibility: 0.10, - expansionPotential: 0.02, - flexibility: 0.02, - }, - confidenceInCriteria: 0.92, - organizationId: 'org-wincasa', - createdAt: '2025-05-02T08:30:00Z', - updatedAt: '2025-05-09T16:00:00Z', - }, - - // --- need-007: Creative Studios AG — MIXED Zürich/Bern --- - { - id: 'need-007', - companyName: 'Creative Studios AG', - contactName: 'Nora Hauser', - assetType: AssetType.MIXED, - requiredArea: { min: 800, max: 1500 }, - preferredLocations: ['Zürich', 'Zürich-West', 'Zürich Altstetten', 'Bern'], - excludedLocations: [], - budgetRange: { maxPerSqm: 55, maxMonthlyTotal: 75000, currency: 'CHF' }, - timing: { - earliestMoveIn: '2026-01-01', - latestMoveIn: '2026-07-01', - contractDurationMonths: 48, - flexibleTiming: true, - }, - mustCriteriaText: ['Gemischte Nutzung möglich', 'Hohe Decken', 'Kreative Atmosphäre'], - softFactors: { - minPrestige: 55, - minAccessibility: 70, - requireParking: false, - maxPublicTransportMinutes: 12, - }, - 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.82, - extractedFromText: 'Kreativagentur sucht Gewerbe-/Bürofläche in Zürich oder Bern, 900–1400m², gemischte Nutzung, Budget max. CHF 50/m².', - organizationId: 'org-wincasa', - createdAt: '2025-04-05T10:00:00Z', - updatedAt: '2025-05-03T11:00:00Z', - }, - - // --- need-008: Berner Produzenten GmbH — PRODUCTION Bern --- - { - id: 'need-008', - companyName: 'Berner Produzenten GmbH', - contactName: 'Hans Lüthi', - assetType: AssetType.PRODUCTION, - requiredArea: { min: 2000, max: 4000 }, - preferredLocations: ['Bern', 'Brünnen', 'Münchenbuchsee', 'Bern West'], - budgetRange: { maxPerSqm: 14, currency: 'CHF' }, - timing: { - earliestMoveIn: '2025-12-01', - latestMoveIn: '2026-09-01', - contractDurationMonths: 120, - flexibleTiming: false, - }, - mustCriteriaText: ['Kranbahn möglich', 'Hallenhöhe min 8m', 'Drehstrom 400V', 'Lkw-Andienung'], - softFactors: { - requireParking: true, - }, - weightingProfile: { - area: 0.30, - location: 0.20, - budget: 0.20, - timing: 0.10, - prestige: 0.01, - accessibility: 0.10, - expansionPotential: 0.07, - flexibility: 0.02, - }, - confidenceInCriteria: 0.95, - organizationId: 'org-wincasa', - createdAt: '2025-03-15T09:00:00Z', - updatedAt: '2025-04-20T12:00:00Z', - }, - - // --- need-009: Geneva Commerce SA — RETAIL Genf --- - { - id: 'need-009', - companyName: 'Geneva Commerce SA', - contactName: 'Pierre Dupont', - assetType: AssetType.RETAIL, - requiredArea: { min: 150, max: 400 }, - preferredLocations: ['Genf', 'Genf Rive', 'Genf Centre'], - excludedLocations: [], - budgetRange: { maxPerSqm: 120, maxMonthlyTotal: 48000, currency: 'CHF' }, - timing: { - earliestMoveIn: '2026-01-01', - latestMoveIn: '2026-06-01', - contractDurationMonths: 60, - flexibleTiming: false, - }, - mustCriteriaText: ['Centre-ville Genève', 'Vitrine', 'Rez-de-chaussée', 'Passage piétonnier'], - softFactors: { - minPrestige: 88, - requireParking: false, - maxPublicTransportMinutes: 5, - }, - weightingProfile: { - area: 0.15, - location: 0.35, - budget: 0.15, - timing: 0.10, - prestige: 0.15, - accessibility: 0.05, - expansionPotential: 0.02, - flexibility: 0.03, - }, - confidenceInCriteria: 0.93, - extractedFromText: 'Recherche surface commerciale en centre-ville de Genève, 200–350m², vitrine obligatoire, budget max CHF 115/m².', - organizationId: 'org-wincasa', - createdAt: '2025-02-28T15:00:00Z', - updatedAt: '2025-04-18T09:00:00Z', - }, - - // --- need-010: St.Galler Büros AG — OFFICE St.Gallen --- - { - id: 'need-010', - companyName: 'St.Galler Büros AG', - contactName: 'Brigitte Fässler', - assetType: AssetType.OFFICE, - requiredArea: { min: 400, max: 800 }, - preferredLocations: ['St. Gallen', 'St. Gallen Centrum', 'Riethüsli', 'Ostschweiz'], - excludedLocations: [], - budgetRange: { maxPerSqm: 35, maxMonthlyTotal: 28000, currency: 'CHF' }, - timing: { - earliestMoveIn: '2025-11-01', - latestMoveIn: '2026-04-01', - contractDurationMonths: 48, - flexibleTiming: true, - }, - mustCriteriaText: ['Stadtzentrumsnähe', 'ÖV < 8 Min', 'Helligkeit und Ausbauqualität'], - softFactors: { - minPrestige: 60, - minAccessibility: 70, - requireParking: true, - maxPublicTransportMinutes: 8, - }, - weightingProfile: { - area: 0.22, - location: 0.28, - budget: 0.20, - timing: 0.12, - prestige: 0.08, - accessibility: 0.06, - expansionPotential: 0.02, - flexibility: 0.02, - }, - confidenceInCriteria: 0.87, - extractedFromText: 'Büroflächen in St. Gallen oder Umgebung gesucht, 450–750m², max. CHF 32/m², Bezug Anfang 2026.', - organizationId: 'org-wincasa', - createdAt: '2025-04-10T08:00:00Z', - updatedAt: '2025-05-06T13:00:00Z', - }, -] diff --git a/.claude/worktrees/agent-a82a3716/src/mock-data/properties.ts b/.claude/worktrees/agent-a82a3716/src/mock-data/properties.ts deleted file mode 100644 index bf49fc2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/mock-data/properties.ts +++ /dev/null @@ -1,975 +0,0 @@ -import { AssetType, ResultType, AvailabilityStatus, DataFreshness, RiskLevel } from '../domain/enums' -import type { Property } from '../domain/property' - -export const mockProperties: Property[] = [ - - // ───────────────────────────────────────────────────────────────────────────── - // VERIFIED_PORTFOLIO (10) - // ───────────────────────────────────────────────────────────────────────────── - - { - 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', - }, - - { - id: 'prop-007', - title: 'Bürofläche Thurgauerstrasse 40', - assetType: AssetType.OFFICE, - resultType: ResultType.VERIFIED_PORTFOLIO, - location: { city: 'Zürich', district: 'Oerlikon', canton: 'ZH', country: 'CH', coordinates: { lat: 47.4115, lng: 8.5502 } }, - address: { street: 'Thurgauerstrasse', houseNumber: '40', postalCode: '8050', city: 'Zürich', country: 'CH' }, - areaSqm: 720, - rentPricePerSqm: 36, - totalRentMonthly: 25920, - availabilityDate: '2025-10-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, - sourceType: 'ERP_IMPORT', - confidenceScore: 0.98, - dataQuality: { - score: 0.94, - missingCriticalFields: [], - missingOptionalFields: [], - lastVerifiedAt: '2025-05-01', - freshness: DataFreshness.FRESH, - warnings: [], - }, - softFactors: { - prestige: 72, - accessibility: 88, - visibilityScore: 60, - talentAccess: 80, - parkingSpots: 8, - publicTransportMinutes: 5, - }, - floorLevel: 2, - expansionPotentialSqm: 200, - contractDurationMonths: 48, - ancillaryCosts: 5.0, - riskLevel: RiskLevel.LOW, - organizationId: 'org-wincasa', - createdAt: '2025-02-01T09:00:00Z', - updatedAt: '2025-05-01T08:00:00Z', - }, - - { - id: 'prop-008', - title: 'Bürofläche Dreispitz Areal 9', - assetType: AssetType.OFFICE, - resultType: ResultType.VERIFIED_PORTFOLIO, - location: { city: 'Basel', district: 'Dreispitz', canton: 'BS', country: 'CH', coordinates: { lat: 47.5398, lng: 7.5812 } }, - address: { street: 'Hochbergerstrasse', houseNumber: '9', postalCode: '4057', city: 'Basel', country: 'CH' }, - areaSqm: 900, - rentPricePerSqm: 32, - totalRentMonthly: 28800, - availabilityDate: '2025-09-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, - sourceType: 'ERP_IMPORT', - confidenceScore: 0.97, - dataQuality: { - score: 0.93, - missingCriticalFields: [], - missingOptionalFields: ['expansionPotentialSqm'], - lastVerifiedAt: '2025-04-30', - freshness: DataFreshness.FRESH, - warnings: [], - }, - softFactors: { - prestige: 70, - accessibility: 82, - visibilityScore: 58, - talentAccess: 72, - parkingSpots: 14, - publicTransportMinutes: 8, - }, - floorLevel: 4, - contractDurationMonths: 48, - ancillaryCosts: 4.5, - riskLevel: RiskLevel.LOW, - organizationId: 'org-wincasa', - createdAt: '2025-01-20T10:00:00Z', - updatedAt: '2025-04-30T09:00:00Z', - }, - - { - id: 'prop-009', - title: 'Logistikzentrum Tössfeldstrasse 18', - assetType: AssetType.LOGISTICS, - resultType: ResultType.VERIFIED_PORTFOLIO, - location: { city: 'Winterthur', district: 'Töss', canton: 'ZH', country: 'CH', coordinates: { lat: 47.4952, lng: 8.7082 } }, - address: { street: 'Tössfeldstrasse', houseNumber: '18', postalCode: '8406', city: 'Winterthur', country: 'CH' }, - areaSqm: 1800, - rentPricePerSqm: 13, - totalRentMonthly: 23400, - availabilityDate: '2025-07-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, - sourceType: 'ERP_IMPORT', - confidenceScore: 0.98, - dataQuality: { - score: 0.95, - missingCriticalFields: [], - missingOptionalFields: [], - lastVerifiedAt: '2025-05-02', - freshness: DataFreshness.FRESH, - warnings: [], - }, - softFactors: { - prestige: 38, - accessibility: 85, - parkingSpots: 25, - publicTransportMinutes: 14, - }, - floorLevel: 0, - expansionPotentialSqm: 600, - contractDurationMonths: 60, - ancillaryCosts: 2.8, - riskLevel: RiskLevel.LOW, - organizationId: 'org-wincasa', - createdAt: '2024-12-10T08:00:00Z', - updatedAt: '2025-05-02T10:00:00Z', - }, - - { - id: 'prop-010', - title: 'Retailfläche Löwenplatz 3', - assetType: AssetType.RETAIL, - resultType: ResultType.VERIFIED_PORTFOLIO, - location: { city: 'Zürich', district: 'Innenstadt', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3758, lng: 8.5352 } }, - address: { street: 'Löwenplatz', houseNumber: '3', postalCode: '8001', city: 'Zürich', country: 'CH' }, - areaSqm: 285, - rentPricePerSqm: 88, - totalRentMonthly: 25080, - availabilityDate: '2025-08-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, - sourceType: 'ERP_IMPORT', - confidenceScore: 0.99, - dataQuality: { - score: 0.96, - missingCriticalFields: [], - missingOptionalFields: [], - lastVerifiedAt: '2025-05-06', - freshness: DataFreshness.FRESH, - warnings: [], - }, - softFactors: { - prestige: 95, - visibilityScore: 98, - passerbyFrequency: 'VERY_HIGH', - accessibility: 96, - publicTransportMinutes: 2, - }, - floorLevel: 0, - contractDurationMonths: 60, - ancillaryCosts: 8.0, - riskLevel: RiskLevel.LOW, - organizationId: 'org-wincasa', - createdAt: '2025-01-05T10:00:00Z', - updatedAt: '2025-05-06T11:00:00Z', - }, - - { - id: 'prop-011', - title: 'Produktionshalle Brünnen West 22', - assetType: AssetType.PRODUCTION, - resultType: ResultType.VERIFIED_PORTFOLIO, - location: { city: 'Bern', district: 'Brünnen', canton: 'BE', country: 'CH', coordinates: { lat: 46.9562, lng: 7.3818 } }, - address: { street: 'Brünnenstrasse', houseNumber: '22', postalCode: '3018', city: 'Bern', country: 'CH' }, - areaSqm: 2800, - rentPricePerSqm: 12, - totalRentMonthly: 33600, - availabilityDate: '2025-07-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, - sourceType: 'ERP_IMPORT', - confidenceScore: 0.98, - dataQuality: { - score: 0.95, - missingCriticalFields: [], - missingOptionalFields: [], - lastVerifiedAt: '2025-05-03', - freshness: DataFreshness.FRESH, - warnings: [], - }, - softFactors: { - prestige: 35, - accessibility: 80, - parkingSpots: 40, - publicTransportMinutes: 18, - }, - floorLevel: 0, - expansionPotentialSqm: 1200, - contractDurationMonths: 120, - ancillaryCosts: 2.5, - riskLevel: RiskLevel.LOW, - organizationId: 'org-wincasa', - createdAt: '2024-10-15T09:00:00Z', - updatedAt: '2025-05-03T10:00:00Z', - }, - - { - id: 'prop-012', - title: 'Bürofläche Stadtturm Zug', - assetType: AssetType.OFFICE, - resultType: ResultType.VERIFIED_PORTFOLIO, - location: { city: 'Zug', district: 'Zentrum', canton: 'ZG', country: 'CH', coordinates: { lat: 47.1712, lng: 8.5150 } }, - address: { street: 'Industriestrasse', houseNumber: '2', postalCode: '6300', city: 'Zug', country: 'CH' }, - areaSqm: 550, - rentPricePerSqm: 42, - totalRentMonthly: 23100, - availabilityDate: '2025-10-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, - sourceType: 'ERP_IMPORT', - confidenceScore: 0.97, - dataQuality: { - score: 0.93, - missingCriticalFields: [], - missingOptionalFields: [], - lastVerifiedAt: '2025-04-29', - freshness: DataFreshness.FRESH, - warnings: [], - }, - softFactors: { - prestige: 82, - accessibility: 88, - visibilityScore: 70, - talentAccess: 78, - parkingSpots: 6, - publicTransportMinutes: 6, - }, - floorLevel: 5, - contractDurationMonths: 36, - ancillaryCosts: 6.0, - riskLevel: RiskLevel.LOW, - organizationId: 'org-wincasa', - createdAt: '2025-02-10T11:00:00Z', - updatedAt: '2025-04-29T08:00:00Z', - }, - - { - id: 'prop-013', - title: 'Gewerbe-/Bürofläche Altstetten Park', - assetType: AssetType.MIXED, - resultType: ResultType.VERIFIED_PORTFOLIO, - location: { city: 'Zürich', district: 'Altstetten', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3908, lng: 8.4888 } }, - address: { street: 'Badenerstrasse', houseNumber: '810', postalCode: '8048', city: 'Zürich', country: 'CH' }, - areaSqm: 1300, - rentPricePerSqm: 45, - totalRentMonthly: 58500, - availabilityDate: '2025-11-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, - sourceType: 'ERP_IMPORT', - confidenceScore: 0.96, - dataQuality: { - score: 0.92, - missingCriticalFields: [], - missingOptionalFields: ['expansionPotentialSqm'], - lastVerifiedAt: '2025-04-25', - freshness: DataFreshness.FRESH, - warnings: [], - }, - softFactors: { - prestige: 62, - accessibility: 84, - visibilityScore: 55, - talentAccess: 72, - parkingSpots: 18, - publicTransportMinutes: 7, - }, - floorLevel: 1, - contractDurationMonths: 48, - ancillaryCosts: 5.0, - riskLevel: RiskLevel.LOW, - organizationId: 'org-wincasa', - createdAt: '2025-01-15T09:00:00Z', - updatedAt: '2025-04-25T10:00:00Z', - }, - - { - id: 'prop-014', - title: 'Logistikhalle Pratteln Nord', - assetType: AssetType.LOGISTICS, - resultType: ResultType.VERIFIED_PORTFOLIO, - location: { city: 'Pratteln', district: 'Industriezone', canton: 'BL', country: 'CH', coordinates: { lat: 47.5228, lng: 7.6958 } }, - address: { street: 'Industriestrasse', houseNumber: '55', postalCode: '4133', city: 'Pratteln', country: 'CH' }, - areaSqm: 3100, - rentPricePerSqm: 15, - totalRentMonthly: 46500, - availabilityDate: '2025-07-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_NOW, - sourceType: 'ERP_IMPORT', - confidenceScore: 0.98, - dataQuality: { - score: 0.94, - missingCriticalFields: [], - missingOptionalFields: [], - lastVerifiedAt: '2025-05-04', - freshness: DataFreshness.FRESH, - warnings: [], - }, - softFactors: { - prestige: 42, - accessibility: 90, - parkingSpots: 45, - publicTransportMinutes: 16, - }, - floorLevel: 0, - expansionPotentialSqm: 1500, - contractDurationMonths: 60, - ancillaryCosts: 2.8, - riskLevel: RiskLevel.LOW, - organizationId: 'org-wincasa', - createdAt: '2024-12-01T08:00:00Z', - updatedAt: '2025-05-04T09:00:00Z', - }, - - // ───────────────────────────────────────────────────────────────────────────── - // EXTERNAL_MARKET (10) - // ───────────────────────────────────────────────────────────────────────────── - - { - 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', - }, - - { - id: 'prop-015', - title: 'Bürofläche Kasernenplatz Luzern', - assetType: AssetType.OFFICE, - resultType: ResultType.EXTERNAL_MARKET, - location: { city: 'Luzern', district: 'Innenstadt', canton: 'LU', country: 'CH', coordinates: { lat: 47.0502, lng: 8.3093 } }, - address: { street: 'Kasernenplatz', houseNumber: '3', postalCode: '6003', city: 'Luzern', country: 'CH' }, - areaSqm: 650, - rentPricePerSqm: 38, - totalRentMonthly: 24700, - availabilityDate: '2025-10-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, - sourceType: 'HOMEGATE_SCRAPE', - sourceUrl: 'https://example.com/listing/prop-015', - confidenceScore: 0.70, - dataQuality: { - score: 0.60, - missingCriticalFields: ['contractDurationMonths'], - missingOptionalFields: ['ancillaryCosts', 'expansionPotentialSqm'], - lastVerifiedAt: '2025-04-05', - freshness: DataFreshness.STALE, - warnings: ['Verfügbarkeit aus Drittquelle – nicht bestätigt'], - }, - softFactors: { - prestige: 74, - accessibility: 86, - publicTransportMinutes: 5, - }, - riskLevel: RiskLevel.MEDIUM, - createdAt: '2025-03-12T11:00:00Z', - updatedAt: '2025-04-05T10:00:00Z', - }, - - { - id: 'prop-016', - title: 'Logistikhalle Muttenz Rheinfelderstrasse', - assetType: AssetType.LOGISTICS, - resultType: ResultType.EXTERNAL_MARKET, - location: { city: 'Muttenz', district: 'Industriezone', canton: 'BL', country: 'CH', coordinates: { lat: 47.5202, lng: 7.6422 } }, - address: { street: 'Rheinfelderstrasse', houseNumber: '80', postalCode: '4132', city: 'Muttenz', country: 'CH' }, - areaSqm: 2200, - rentPricePerSqm: 16, - totalRentMonthly: 35200, - availabilityDate: '2025-09-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, - sourceType: 'IMMOSCOUT_SCRAPE', - sourceUrl: 'https://example.com/listing/prop-016', - confidenceScore: 0.69, - dataQuality: { - score: 0.58, - missingCriticalFields: ['contractDurationMonths'], - missingOptionalFields: ['ancillaryCosts'], - lastVerifiedAt: '2025-03-28', - freshness: DataFreshness.STALE, - warnings: ['Hallenhöhe nicht angegeben', 'Daten nicht verifiziert'], - }, - softFactors: { - prestige: 42, - accessibility: 88, - parkingSpots: 35, - }, - riskLevel: RiskLevel.MEDIUM, - createdAt: '2025-02-20T09:00:00Z', - updatedAt: '2025-03-28T12:00:00Z', - }, - - { - id: 'prop-017', - title: 'Ladenfläche Löwenstrasse 28', - assetType: AssetType.RETAIL, - resultType: ResultType.EXTERNAL_MARKET, - location: { city: 'Zürich', district: 'Innenstadt', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3766, lng: 8.5385 } }, - address: { street: 'Löwenstrasse', houseNumber: '28', postalCode: '8001', city: 'Zürich', country: 'CH' }, - areaSqm: 350, - rentPricePerSqm: 95, - totalRentMonthly: 33250, - availabilityDate: '2025-09-15', - availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, - sourceType: 'MATCHOFFICE_SCRAPE', - sourceUrl: 'https://example.com/listing/prop-017', - confidenceScore: 0.71, - dataQuality: { - score: 0.61, - missingCriticalFields: ['contractDurationMonths'], - missingOptionalFields: ['floorLevel', 'ancillaryCosts'], - lastVerifiedAt: '2025-04-18', - freshness: DataFreshness.STALE, - warnings: ['Mietpreis nicht final bestätigt'], - }, - softFactors: { - prestige: 90, - visibilityScore: 94, - passerbyFrequency: 'VERY_HIGH', - publicTransportMinutes: 3, - }, - riskLevel: RiskLevel.MEDIUM, - createdAt: '2025-03-05T13:00:00Z', - updatedAt: '2025-04-18T11:00:00Z', - }, - - { - id: 'prop-018', - title: 'Bürofläche Breitenrain 14', - assetType: AssetType.OFFICE, - resultType: ResultType.EXTERNAL_MARKET, - location: { city: 'Bern', district: 'Breitenrain', canton: 'BE', country: 'CH', coordinates: { lat: 46.9598, lng: 7.4522 } }, - address: { street: 'Breitenrainstrasse', houseNumber: '14', postalCode: '3014', city: 'Bern', country: 'CH' }, - areaSqm: 780, - rentPricePerSqm: 31, - totalRentMonthly: 24180, - availabilityDate: '2025-10-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, - sourceType: 'NEWHOME_SCRAPE', - sourceUrl: 'https://example.com/listing/prop-018', - confidenceScore: 0.68, - dataQuality: { - score: 0.57, - missingCriticalFields: ['contractDurationMonths', 'ancillaryCosts'], - missingOptionalFields: ['floorLevel'], - lastVerifiedAt: '2025-04-02', - freshness: DataFreshness.STALE, - warnings: ['Daten aus Drittquelle', 'Renovierungsstand unklar'], - }, - softFactors: { - prestige: 62, - accessibility: 78, - publicTransportMinutes: 8, - }, - riskLevel: RiskLevel.MEDIUM, - createdAt: '2025-02-28T10:00:00Z', - updatedAt: '2025-04-02T09:00:00Z', - }, - - { - id: 'prop-019', - title: 'Produktionsfläche Voltastrasse Basel', - assetType: AssetType.PRODUCTION, - resultType: ResultType.EXTERNAL_MARKET, - location: { city: 'Basel', district: 'Kleinhüningen', canton: 'BS', country: 'CH', coordinates: { lat: 47.5720, lng: 7.5882 } }, - address: { street: 'Voltastrasse', houseNumber: '62', postalCode: '4056', city: 'Basel', country: 'CH' }, - areaSqm: 1900, - rentPricePerSqm: 13, - totalRentMonthly: 24700, - availabilityDate: '2025-11-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, - sourceType: 'IMMOSCOUT_SCRAPE', - sourceUrl: 'https://example.com/listing/prop-019', - confidenceScore: 0.68, - dataQuality: { - score: 0.56, - missingCriticalFields: ['contractDurationMonths', 'ancillaryCosts'], - missingOptionalFields: ['softFactors'], - lastVerifiedAt: '2025-03-25', - freshness: DataFreshness.STALE, - warnings: ['Hallenhöhe nicht verifiziert', 'Kranbahn Status unklar'], - }, - riskLevel: RiskLevel.MEDIUM, - createdAt: '2025-03-10T08:00:00Z', - updatedAt: '2025-03-25T14:00:00Z', - }, - - { - id: 'prop-020', - title: 'Bürofläche Industriestrasse Zug', - assetType: AssetType.OFFICE, - resultType: ResultType.EXTERNAL_MARKET, - location: { city: 'Zug', district: 'Industrie', canton: 'ZG', country: 'CH', coordinates: { lat: 47.1688, lng: 8.5228 } }, - address: { street: 'Industriestrasse', houseNumber: '45', postalCode: '6300', city: 'Zug', country: 'CH' }, - areaSqm: 820, - rentPricePerSqm: 44, - totalRentMonthly: 36080, - availabilityDate: '2025-11-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, - sourceType: 'HOMEGATE_SCRAPE', - sourceUrl: 'https://example.com/listing/prop-020', - confidenceScore: 0.70, - dataQuality: { - score: 0.60, - missingCriticalFields: ['contractDurationMonths'], - missingOptionalFields: ['ancillaryCosts', 'expansionPotentialSqm'], - lastVerifiedAt: '2025-04-12', - freshness: DataFreshness.STALE, - warnings: ['Ausbaustandard nicht bestätigt'], - }, - softFactors: { - prestige: 68, - accessibility: 82, - publicTransportMinutes: 9, - }, - riskLevel: RiskLevel.MEDIUM, - createdAt: '2025-03-18T09:00:00Z', - updatedAt: '2025-04-12T11:00:00Z', - }, - - { - id: 'prop-021', - title: 'Surface commerciale Rue du Rhône', - assetType: AssetType.RETAIL, - resultType: ResultType.EXTERNAL_MARKET, - location: { city: 'Genf', district: 'Centre', canton: 'GE', country: 'CH', coordinates: { lat: 46.2044, lng: 6.1432 } }, - address: { street: 'Rue du Rhône', houseNumber: '48', postalCode: '1204', city: 'Genf', country: 'CH' }, - areaSqm: 250, - rentPricePerSqm: 112, - totalRentMonthly: 28000, - availabilityDate: '2026-01-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, - sourceType: 'MATCHOFFICE_SCRAPE', - sourceUrl: 'https://example.com/listing/prop-021', - confidenceScore: 0.70, - dataQuality: { - score: 0.59, - missingCriticalFields: ['contractDurationMonths'], - missingOptionalFields: ['ancillaryCosts', 'floorLevel'], - lastVerifiedAt: '2025-04-08', - freshness: DataFreshness.STALE, - warnings: ['Prix non confirmé', 'Disponibilité à vérifier'], - }, - softFactors: { - prestige: 94, - visibilityScore: 96, - passerbyFrequency: 'VERY_HIGH', - publicTransportMinutes: 3, - }, - riskLevel: RiskLevel.MEDIUM, - createdAt: '2025-02-10T10:00:00Z', - updatedAt: '2025-04-08T09:00:00Z', - }, - - { - id: 'prop-022', - title: 'Bürofläche St.Gallen Centrum 7', - assetType: AssetType.OFFICE, - resultType: ResultType.EXTERNAL_MARKET, - location: { city: 'St. Gallen', district: 'Centrum', canton: 'SG', country: 'CH', coordinates: { lat: 47.4245, lng: 9.3767 } }, - address: { street: 'Marktgasse', houseNumber: '7', postalCode: '9000', city: 'St. Gallen', country: 'CH' }, - areaSqm: 700, - rentPricePerSqm: 28, - totalRentMonthly: 19600, - availabilityDate: '2025-11-01', - availabilityStatus: AvailabilityStatus.AVAILABLE_SOON, - sourceType: 'NEWHOME_SCRAPE', - sourceUrl: 'https://example.com/listing/prop-022', - confidenceScore: 0.69, - dataQuality: { - score: 0.58, - missingCriticalFields: ['contractDurationMonths'], - missingOptionalFields: ['ancillaryCosts', 'floorLevel'], - lastVerifiedAt: '2025-04-14', - freshness: DataFreshness.STALE, - warnings: ['Daten aus Drittquelle', 'Ausbauqualität nicht bestätigt'], - }, - softFactors: { - prestige: 66, - accessibility: 80, - publicTransportMinutes: 6, - }, - riskLevel: RiskLevel.MEDIUM, - createdAt: '2025-03-08T08:00:00Z', - updatedAt: '2025-04-14T10:00:00Z', - }, - - // ───────────────────────────────────────────────────────────────────────────── - // FUTURE_AVAILABILITY (10) - // ───────────────────────────────────────────────────────────────────────────── - - { - 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', - }, - - { - id: 'prop-023', - title: 'Bürofläche Zürich-Nord Seebach (Signal: Auszug)', - assetType: AssetType.OFFICE, - resultType: ResultType.FUTURE_AVAILABILITY, - location: { city: 'Zürich', district: 'Seebach', canton: 'ZH', country: 'CH', coordinates: { lat: 47.4298, lng: 8.5362 } }, - address: { street: 'Binzmühlestrasse', houseNumber: '95', postalCode: '8050', city: 'Zürich', country: 'CH' }, - areaSqm: 850, - rentPricePerSqm: 38, - availabilityDate: '2026-02-01', - availabilityStatus: AvailabilityStatus.FUTURE_SIGNAL, - sourceType: 'AI_SIGNAL', - confidenceScore: 0.54, - dataQuality: { - score: 0.36, - missingCriticalFields: ['totalRentMonthly', 'contractDurationMonths'], - missingOptionalFields: ['ancillaryCosts', 'floorLevel', 'softFactors'], - freshness: DataFreshness.FRESH, - warnings: ['Probabilistisches Signal – kein bestätigtes Objekt', 'Mietpreis geschätzt'], - }, - riskLevel: RiskLevel.HIGH, - createdAt: '2025-04-14T08:00:00Z', - updatedAt: '2025-05-10T08:00:00Z', - }, - - { - id: 'prop-024', - title: 'Logistikneubau Basel Hafen Klybeck (Signal: Neubau)', - assetType: AssetType.LOGISTICS, - resultType: ResultType.FUTURE_AVAILABILITY, - location: { city: 'Basel', district: 'Klybeck', canton: 'BS', country: 'CH', coordinates: { lat: 47.5762, lng: 7.5918 } }, - address: { street: 'Klybeckstrasse', houseNumber: '180', postalCode: '4057', city: 'Basel', country: 'CH' }, - areaSqm: 2600, - rentPricePerSqm: 14, - availabilityDate: '2026-07-01', - availabilityStatus: AvailabilityStatus.FUTURE_SIGNAL, - sourceType: 'AI_SIGNAL', - confidenceScore: 0.52, - dataQuality: { - score: 0.35, - missingCriticalFields: ['totalRentMonthly', 'contractDurationMonths'], - missingOptionalFields: ['ancillaryCosts', 'softFactors'], - freshness: DataFreshness.FRESH, - warnings: ['Baubewilligung erteilt, Mieter noch nicht bekannt', 'Konditionen geschätzt'], - }, - riskLevel: RiskLevel.MEDIUM, - createdAt: '2025-01-20T09:00:00Z', - updatedAt: '2025-05-10T09:00:00Z', - }, - - { - id: 'prop-025', - title: 'Retailfläche Zürich Niederdorf (Signal: Auszug)', - assetType: AssetType.RETAIL, - resultType: ResultType.FUTURE_AVAILABILITY, - location: { city: 'Zürich', district: 'Niederdorf', canton: 'ZH', country: 'CH', coordinates: { lat: 47.3748, lng: 8.5418 } }, - address: { street: 'Münstergasse', houseNumber: '14', postalCode: '8001', city: 'Zürich', country: 'CH' }, - areaSqm: 280, - rentPricePerSqm: 92, - availabilityDate: '2026-09-01', - availabilityStatus: AvailabilityStatus.FUTURE_SIGNAL, - sourceType: 'AI_SIGNAL', - confidenceScore: 0.50, - dataQuality: { - score: 0.33, - missingCriticalFields: ['totalRentMonthly', 'contractDurationMonths'], - missingOptionalFields: ['ancillaryCosts', 'floorLevel'], - freshness: DataFreshness.FRESH, - warnings: ['Probabilistisches Signal – kein bestätigtes Objekt', 'Preis geschätzt'], - }, - riskLevel: RiskLevel.HIGH, - createdAt: '2025-04-02T08:00:00Z', - updatedAt: '2025-05-10T08:00:00Z', - }, - - { - id: 'prop-026', - title: 'Bürofläche Zug Industriestrasse (Signal: Expansion)', - assetType: AssetType.OFFICE, - resultType: ResultType.FUTURE_AVAILABILITY, - location: { city: 'Zug', district: 'Industrie', canton: 'ZG', country: 'CH', coordinates: { lat: 47.1672, lng: 8.5198 } }, - address: { street: 'Industriestrasse', houseNumber: '60', postalCode: '6300', city: 'Zug', country: 'CH' }, - areaSqm: 580, - rentPricePerSqm: 43, - availabilityDate: '2026-04-01', - availabilityStatus: AvailabilityStatus.FUTURE_SIGNAL, - sourceType: 'AI_SIGNAL', - confidenceScore: 0.52, - dataQuality: { - score: 0.34, - missingCriticalFields: ['totalRentMonthly', 'contractDurationMonths'], - missingOptionalFields: ['ancillaryCosts', 'floorLevel', 'softFactors'], - freshness: DataFreshness.FRESH, - warnings: ['Probabilistisches Signal – kein bestätigtes Objekt', 'Lage approximiert'], - }, - riskLevel: RiskLevel.HIGH, - createdAt: '2025-04-22T07:00:00Z', - updatedAt: '2025-05-10T08:00:00Z', - }, - - { - id: 'prop-027', - title: 'Produktionsfläche Münchenbuchsee BE (Signal: Auszug)', - assetType: AssetType.PRODUCTION, - resultType: ResultType.FUTURE_AVAILABILITY, - location: { city: 'Münchenbuchsee', district: 'Industriezone', canton: 'BE', country: 'CH', coordinates: { lat: 47.0038, lng: 7.4542 } }, - address: { street: 'Bernstrasse', houseNumber: '42', postalCode: '3053', city: 'Münchenbuchsee', country: 'CH' }, - areaSqm: 2200, - rentPricePerSqm: 12, - availabilityDate: '2026-08-01', - availabilityStatus: AvailabilityStatus.FUTURE_SIGNAL, - sourceType: 'AI_SIGNAL', - confidenceScore: 0.48, - dataQuality: { - score: 0.32, - missingCriticalFields: ['totalRentMonthly', 'contractDurationMonths'], - missingOptionalFields: ['ancillaryCosts', 'softFactors'], - freshness: DataFreshness.FRESH, - warnings: ['Probabilistisches Signal – kein bestätigtes Objekt', 'Daten nicht verifiziert'], - }, - riskLevel: RiskLevel.HIGH, - createdAt: '2025-03-14T08:00:00Z', - updatedAt: '2025-05-09T09:00:00Z', - }, - - { - id: 'prop-028', - title: 'Bürofläche Genf La Praille (Signal: Expansion)', - assetType: AssetType.OFFICE, - resultType: ResultType.FUTURE_AVAILABILITY, - location: { city: 'Genf', district: 'La Praille', canton: 'GE', country: 'CH', coordinates: { lat: 46.1912, lng: 6.1285 } }, - address: { street: 'Route de la Praille', houseNumber: '30', postalCode: '1227', city: 'Genf', country: 'CH' }, - areaSqm: 520, - rentPricePerSqm: 36, - availabilityDate: '2027-01-01', - availabilityStatus: AvailabilityStatus.FUTURE_SIGNAL, - sourceType: 'AI_SIGNAL', - confidenceScore: 0.53, - dataQuality: { - score: 0.35, - missingCriticalFields: ['totalRentMonthly', 'contractDurationMonths'], - missingOptionalFields: ['ancillaryCosts', 'floorLevel'], - freshness: DataFreshness.FRESH, - warnings: ['Probabilistisches Signal – Standort approximiert', 'Mietzins geschätzt'], - }, - riskLevel: RiskLevel.HIGH, - createdAt: '2025-03-10T09:00:00Z', - updatedAt: '2025-05-09T10:00:00Z', - }, - - { - id: 'prop-029', - title: 'Logistiklager Frenkendorf BL (Signal: Auszug)', - assetType: AssetType.LOGISTICS, - resultType: ResultType.FUTURE_AVAILABILITY, - location: { city: 'Frenkendorf', district: 'Industriezone', canton: 'BL', country: 'CH', coordinates: { lat: 47.5098, lng: 7.7182 } }, - address: { street: 'Frenkenstrasse', houseNumber: '28', postalCode: '4402', city: 'Frenkendorf', country: 'CH' }, - areaSqm: 3500, - rentPricePerSqm: 13, - availabilityDate: '2026-10-01', - availabilityStatus: AvailabilityStatus.FUTURE_SIGNAL, - sourceType: 'AI_SIGNAL', - confidenceScore: 0.46, - dataQuality: { - score: 0.31, - missingCriticalFields: ['totalRentMonthly', 'contractDurationMonths'], - missingOptionalFields: ['ancillaryCosts', 'softFactors'], - freshness: DataFreshness.FRESH, - warnings: ['Probabilistisches Signal – kein bestätigtes Objekt', 'Konditionen unbekannt'], - }, - riskLevel: RiskLevel.HIGH, - createdAt: '2025-04-08T08:00:00Z', - updatedAt: '2025-05-10T08:00:00Z', - }, - - { - id: 'prop-030', - title: 'Bürofläche St.Gallen Riethüsli (Signal: Expansion)', - assetType: AssetType.OFFICE, - resultType: ResultType.FUTURE_AVAILABILITY, - location: { city: 'St. Gallen', district: 'Riethüsli', canton: 'SG', country: 'CH', coordinates: { lat: 47.4182, lng: 9.3888 } }, - address: { street: 'Riethüslistrasse', houseNumber: '40', postalCode: '9000', city: 'St. Gallen', country: 'CH' }, - areaSqm: 480, - rentPricePerSqm: 27, - availabilityDate: '2026-05-01', - availabilityStatus: AvailabilityStatus.FUTURE_SIGNAL, - sourceType: 'AI_SIGNAL', - confidenceScore: 0.55, - dataQuality: { - score: 0.36, - missingCriticalFields: ['totalRentMonthly', 'contractDurationMonths'], - missingOptionalFields: ['ancillaryCosts', 'floorLevel', 'softFactors'], - freshness: DataFreshness.FRESH, - warnings: ['Probabilistisches Signal – kein bestätigtes Objekt', 'Lage und Preis approximiert'], - }, - riskLevel: RiskLevel.HIGH, - createdAt: '2025-04-26T07:00:00Z', - updatedAt: '2025-05-10T08:00:00Z', - }, -] diff --git a/.claude/worktrees/agent-a82a3716/src/mock-data/reviewQueue.ts b/.claude/worktrees/agent-a82a3716/src/mock-data/reviewQueue.ts deleted file mode 100644 index e9f51f2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/mock-data/reviewQueue.ts +++ /dev/null @@ -1,187 +0,0 @@ -import type { ReviewTask } from '../domain/review' - -export const mockReviewQueue: ReviewTask[] = [ - { - id: 'rev-001', - entityType: 'FUTURE_SIGNAL', - entityId: 'sig-001', - title: 'Vertrauliches Signal: Auszug Zürich-Nord Seebach', - description: 'Signal mit Sensitivity RESTRICTED — muss vor Anzeige im Demand Feed manuell geprüft werden. KI-Konfidenz 82%. Quelle: Stellenausschreibungen.', - priority: 'CRITICAL', - status: 'PENDING', - createdBy: 'system', - createdAt: '2026-05-15T08:00:00Z', - updatedAt: '2026-05-15T08:00:00Z', - dueDate: '2026-05-19T17:00:00Z', - reviewNotes: [], - relatedOrganizationId: 'org-wincasa', - confidenceScore: 0.82, - riskLevel: 'HIGH', - }, - { - id: 'rev-002', - entityType: 'LOW_CONFIDENCE_MATCH', - entityId: 'match-021', - title: 'Niedriger Konfidenz-Match: Bern Wabern Office', - description: 'Match-Score 47 mit Konfidenz 0.38. Fehlende Daten: Mietpreis, Ausbaustandard. AI-Begründung unsicher.', - priority: 'HIGH', - status: 'PENDING', - createdBy: 'system', - createdAt: '2026-05-15T09:30:00Z', - updatedAt: '2026-05-15T09:30:00Z', - dueDate: '2026-05-22T17:00:00Z', - reviewNotes: [], - relatedOrganizationId: 'org-wincasa', - confidenceScore: 0.38, - riskLevel: 'MEDIUM', - matchId: 'match-021', - matchScore: 47, - }, - { - id: 'rev-003', - entityType: 'CONTACT_RELEASE', - entityId: 'need-005', - title: 'Kontaktfreigabe: Mobimo Management AG → Zollstrasse 12', - description: 'Nachfrager Mobimo AG bittet um Direktkontakt mit Eigentümer Wincasa. Anfrage durch property-manager prüfen.', - priority: 'HIGH', - status: 'PENDING', - createdBy: 'user-dem', - createdAt: '2026-05-15T11:00:00Z', - updatedAt: '2026-05-15T11:00:00Z', - dueDate: '2026-05-20T17:00:00Z', - reviewNotes: [ - { - id: 'note-001', - content: 'Nachfrager hat Bonität A+ bei CRIF. Anfrage scheint seriös.', - createdBy: 'admin@ideal-sharing.ch', - createdAt: '2026-05-15T12:00:00Z', - }, - ], - relatedOrganizationId: 'org-wincasa', - confidenceScore: 0.91, - riskLevel: 'LOW', - }, - { - id: 'rev-004', - entityType: 'MATCH_EXPLANATION', - entityId: 'match-008', - title: 'Match-Begründung prüfen: Basel Logistik', - description: 'AI-Erklärung für Match enthält widersprüchliche Faktoren. Konfidenz niedrig bei Soft Factors.', - priority: 'HIGH', - status: 'IN_REVIEW', - assignedTo: 'user-rev', - createdBy: 'system', - createdAt: '2026-05-14T14:00:00Z', - updatedAt: '2026-05-15T08:30:00Z', - dueDate: '2026-05-18T17:00:00Z', - reviewNotes: [ - { - id: 'note-002', - content: 'Soft Factor "Passantenfrequenz" ist für Logistik nicht relevant. Score neu berechnen.', - createdBy: 'reviewer@ideal-sharing.ch', - createdAt: '2026-05-15T08:30:00Z', - }, - ], - relatedOrganizationId: 'org-mobimo', - confidenceScore: 0.55, - riskLevel: 'MEDIUM', - matchId: 'match-008', - matchScore: 63, - }, - { - id: 'rev-005', - entityType: 'AI_OUTPUT', - entityId: 'ai-summary-zug-001', - title: 'AI Portfolio-Zusammenfassung: Zug Kantonsstrasse', - description: 'Automatisch generierte Marktzusammenfassung für Zug-Portfolio. Vor Weitergabe an Eigentümer prüfen.', - priority: 'MEDIUM', - status: 'PENDING', - createdBy: 'system', - createdAt: '2026-05-14T10:00:00Z', - updatedAt: '2026-05-14T10:00:00Z', - reviewNotes: [], - relatedOrganizationId: 'org-ubs', - confidenceScore: 0.72, - riskLevel: 'LOW', - promptVersion: 'claude-3-5-sonnet-v1.2', - }, - { - id: 'rev-006', - entityType: 'PROPERTY_DATA_ISSUE', - entityId: 'prop-031', - title: 'Datenfehler: Mietpreis fehlt — Bürofläche Winterthur', - description: 'Pflichtfeld Mietpreis/m² fehlt seit Import. Match-Scoring wird blockiert. Quelle: HomegateImport 2026-05-10.', - priority: 'MEDIUM', - status: 'NEEDS_MORE_DATA', - createdBy: 'system', - createdAt: '2026-05-13T07:00:00Z', - updatedAt: '2026-05-14T16:00:00Z', - reviewNotes: [ - { - id: 'note-003', - content: 'PM wurde benachrichtigt. Wartet auf Rückmeldung vom Eigentümer.', - createdBy: 'admin@ideal-sharing.ch', - createdAt: '2026-05-14T16:00:00Z', - }, - ], - relatedOrganizationId: 'org-wincasa', - riskLevel: 'MEDIUM', - propertyId: 'prop-031', - }, - { - id: 'rev-007', - entityType: 'FUTURE_SIGNAL', - entityId: 'sig-004', - title: 'Signal genehmigt: Baugesuch Basel-Gündeldingen', - description: 'Öffentliches Baugesuch für Büroumbau. Quelle: Kantonales Amtsblatt.', - priority: 'LOW', - status: 'APPROVED', - assignedTo: 'user-rev', - createdBy: 'system', - createdAt: '2026-05-10T09:00:00Z', - updatedAt: '2026-05-12T14:30:00Z', - reviewNotes: [ - { - id: 'note-004', - content: 'Quelle verifiziert. Baugesuch öffentlich zugänglich. Freigabe für Demand Feed.', - createdBy: 'reviewer@ideal-sharing.ch', - createdAt: '2026-05-12T14:30:00Z', - }, - ], - relatedOrganizationId: 'org-wincasa', - confidenceScore: 0.78, - riskLevel: 'LOW', - }, - { - id: 'rev-008', - entityType: 'LOW_CONFIDENCE_MATCH', - entityId: 'match-015', - title: 'Eskaliert: Match Zürich Retail — widersprüchliche Daten', - description: 'Match wurde eskaliert weil Fläche lt. Inserat (320m²) und Katasterdaten (290m²) abweichen.', - priority: 'CRITICAL', - status: 'ESCALATED', - assignedTo: 'user-001', - createdBy: 'user-rev', - createdAt: '2026-05-11T15:00:00Z', - updatedAt: '2026-05-13T09:00:00Z', - reviewNotes: [ - { - id: 'note-005', - content: 'Abweichung 30m² zwischen Inserat und Kataster. Klärung mit Eigentümer notwendig.', - createdBy: 'reviewer@ideal-sharing.ch', - createdAt: '2026-05-12T10:00:00Z', - }, - { - id: 'note-006', - content: 'An Org-Admin eskaliert für finale Entscheidung.', - createdBy: 'reviewer@ideal-sharing.ch', - createdAt: '2026-05-13T09:00:00Z', - }, - ], - relatedOrganizationId: 'org-wincasa', - confidenceScore: 0.44, - riskLevel: 'HIGH', - matchId: 'match-015', - matchScore: 61, - }, -] diff --git a/.claude/worktrees/agent-a82a3716/src/mock-data/shortlists.ts b/.claude/worktrees/agent-a82a3716/src/mock-data/shortlists.ts deleted file mode 100644 index 9fe11ab..0000000 --- a/.claude/worktrees/agent-a82a3716/src/mock-data/shortlists.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { Shortlist } from '../domain/shortlist' -import { ShortlistStatus } from '../domain/enums' - -export const mockShortlists: Shortlist[] = [ - { - id: 'shortlist-001', - title: 'Top Büroflächen Zürich', - description: 'Beste Bürooptionen für Innovatech AG', - needId: 'need-001', - items: [ - { - resultId: 'match-001', - resultType: 'VERIFIED_PORTFOLIO', - title: 'Bürofläche Zollstrasse 12', - matchScore: 88, - confidenceScore: 0.92, - dataQualityScore: 0.85, - sourceLabel: 'Portfolio', - addedAt: '2025-03-01T10:00:00Z', - addedBy: 'admin@ideal-sharing.ch', - note: 'Erste Wahl', - propertyId: 'prop-001', - }, - { - resultId: 'match-004', - resultType: 'VERIFIED_PORTFOLIO', - title: 'Gemischte Gewerbeeinheit Europaallee', - matchScore: 74, - confidenceScore: 0.78, - dataQualityScore: 0.80, - sourceLabel: 'Portfolio', - addedAt: '2025-03-02T14:30:00Z', - addedBy: 'admin@ideal-sharing.ch', - note: 'Interessante Alternative', - propertyId: 'prop-004', - }, - ], - status: ShortlistStatus.ACTIVE, - createdBy: 'user-001', - organizationId: 'org-wincasa', - createdAt: '2025-03-01T10:00:00Z', - updatedAt: '2025-03-02T14:30:00Z', - }, - { - id: 'shortlist-002', - title: 'Logistik Optionen Basel', - description: 'Auswahl für Schweizer Logistik GmbH', - needId: 'need-002', - items: [ - { - resultId: 'match-002', - resultType: 'VERIFIED_PORTFOLIO', - title: 'Lagerfläche Hardstrasse 44', - matchScore: 82, - confidenceScore: 0.88, - dataQualityScore: 0.90, - sourceLabel: 'Portfolio', - addedAt: '2025-03-05T09:00:00Z', - addedBy: 'admin@ideal-sharing.ch', - note: 'Perfekte Grösse', - propertyId: 'prop-002', - }, - ], - status: ShortlistStatus.REVIEW_READY, - createdBy: 'user-001', - organizationId: 'org-wincasa', - sharedWith: ['client@schweizer-logistik.ch'], - createdAt: '2025-03-05T09:00:00Z', - updatedAt: '2025-03-06T11:00:00Z', - }, -] diff --git a/.claude/worktrees/agent-a82a3716/src/mock-data/signalPipelines.ts b/.claude/worktrees/agent-a82a3716/src/mock-data/signalPipelines.ts deleted file mode 100644 index 934ec30..0000000 --- a/.claude/worktrees/agent-a82a3716/src/mock-data/signalPipelines.ts +++ /dev/null @@ -1,860 +0,0 @@ -import type { PipelineState, GateEvaluation, AuditTrailEntry } from '../domain/signalPipeline' -import { PipelineStage, GateType, GateStatus } from '../domain/signalPipeline' - -function gate( - gateType: GateType, - status: GateStatus, - reason: string, - checks: GateEvaluation['checks'], - nextAction?: string, -): GateEvaluation { - return { gateType, status, reason, checks, nextAction, evaluatedAt: '2026-05-15T06:00:00Z' } -} - -// ── sig-001: STAGE_4_REVIEW_CANDIDATE ───────────────────────────────────────── -const sig001Pipeline: PipelineState = { - signalId: 'sig-001', - currentStage: PipelineStage.STAGE_4_REVIEW_CANDIDATE, - overallEligible: false, - publishedToFutureAvailability: false, - gates: { - EVIDENCE_GATE: gate( - GateType.EVIDENCE_GATE, - GateStatus.PASSED, - 'Mindestens ein valides Evidenzstück vorhanden.', - [ - { label: 'Evidenz vorhanden', passed: true, value: '1 Stück' }, - { label: 'Quelle angegeben', passed: true, value: 'Neue Zürcher Zeitung' }, - { label: 'Inhalt nicht leer', passed: true }, - ], - ), - CONFIDENCE_GATE: gate( - GateType.CONFIDENCE_GATE, - GateStatus.PASSED, - 'Konfidenzwert 0.72 liegt über dem Mindestschwellenwert von 0.60.', - [ - { label: 'Konfidenz ≥ 0.60', passed: true, value: '0.72' }, - { label: 'Konfidenz ≥ 0.75 (hoch)', passed: false, value: '0.72', note: 'Mittlere Konfidenz' }, - ], - ), - SENSITIVITY_GATE: gate( - GateType.SENSITIVITY_GATE, - GateStatus.PASSED, - 'Sensitivitätsstufe INTERNAL ist für interne Feed-Nutzung zulässig.', - [ - { label: 'Nicht CONFIDENTIAL', passed: true, value: 'INTERNAL' }, - { label: 'Nicht RESTRICTED', passed: true }, - ], - ), - REVIEW_GATE: gate( - GateType.REVIEW_GATE, - GateStatus.PENDING, - 'Signal wartet auf manuellen Analyst-Review.', - [ - { label: 'Review angefordert', passed: true }, - { label: 'Review abgeschlossen', passed: false }, - ], - 'Analyst-Review durchführen und Signal genehmigen oder ablehnen.', - ), - MATCHABILITY_GATE: gate( - GateType.MATCHABILITY_GATE, - GateStatus.PENDING, - 'Matchbarkeit kann erst nach abgeschlossenem Review bewertet werden.', - [ - { label: 'Asset-Typ definiert', passed: true, value: 'OFFICE' }, - { label: 'Standort definiert', passed: true, value: 'Zürich' }, - { label: 'Zeithorizont definiert', passed: false }, - { label: 'Geschäftsrelevanz vorhanden', passed: true }, - ], - 'Review abschliessen, dann Matchbarkeit neu bewerten.', - ), - FEED_ELIGIBILITY_GATE: gate( - GateType.FEED_ELIGIBILITY_GATE, - GateStatus.BLOCKED, - 'Feed-Eignung blockiert: Review-Gate noch ausstehend.', - [ - { label: 'Review-Gate bestanden', passed: false }, - { label: 'Matchbarkeits-Gate bestanden', passed: false }, - ], - 'Review und Matchbarkeits-Prüfung abschliessen.', - ), - }, -} - -// ── sig-002: STAGE_5_APPROVED_FUTURE ───────────────────────────────────────── -const sig002Pipeline: PipelineState = { - signalId: 'sig-002', - currentStage: PipelineStage.STAGE_5_APPROVED_FUTURE, - overallEligible: true, - publishedToFutureAvailability: false, - gates: { - EVIDENCE_GATE: gate( - GateType.EVIDENCE_GATE, - GateStatus.PASSED, - 'Baugesuchdokument als verlässliche Primärquelle vorhanden.', - [ - { label: 'Evidenz vorhanden', passed: true, value: '1 Stück' }, - { label: 'Quelle angegeben', passed: true, value: 'Baugesuchregister BS' }, - { label: 'Inhalt nicht leer', passed: true }, - ], - ), - CONFIDENCE_GATE: gate( - GateType.CONFIDENCE_GATE, - GateStatus.PASSED, - 'Konfidenzwert 0.88 überschreitet den hohen Schwellenwert von 0.75.', - [ - { label: 'Konfidenz ≥ 0.60', passed: true, value: '0.88' }, - { label: 'Konfidenz ≥ 0.75 (hoch)', passed: true, value: '0.88' }, - ], - ), - SENSITIVITY_GATE: gate( - GateType.SENSITIVITY_GATE, - GateStatus.PASSED, - 'Öffentliche Quelle – keine Einschränkungen.', - [ - { label: 'Nicht CONFIDENTIAL', passed: true, value: 'PUBLIC' }, - { label: 'Nicht RESTRICTED', passed: true }, - ], - ), - REVIEW_GATE: gate( - GateType.REVIEW_GATE, - GateStatus.PASSED, - 'Signal durch Analyst M. Huber am 12.05.2026 genehmigt.', - [ - { label: 'Review angefordert', passed: true }, - { label: 'Review abgeschlossen', passed: true, value: 'M. Huber, 12.05.2026' }, - { label: 'Genehmigt', passed: true }, - ], - ), - MATCHABILITY_GATE: gate( - GateType.MATCHABILITY_GATE, - GateStatus.PASSED, - 'Alle Pflichtfelder für Match-Engine vorhanden.', - [ - { label: 'Asset-Typ definiert', passed: true, value: 'OFFICE, LOGISTICS, RETAIL' }, - { label: 'Standort definiert', passed: true, value: 'Basel' }, - { label: 'Zeithorizont definiert', passed: true, value: 'Q1 2026' }, - { label: 'Geschäftsrelevanz vorhanden', passed: true }, - ], - ), - FEED_ELIGIBILITY_GATE: gate( - GateType.FEED_ELIGIBILITY_GATE, - GateStatus.PASSED, - 'Alle Gates bestanden – Signal ist feed-fähig.', - [ - { label: 'Review-Gate bestanden', passed: true }, - { label: 'Matchbarkeits-Gate bestanden', passed: true }, - { label: 'Sensitivität zulässig', passed: true, value: 'PUBLIC' }, - ], - ), - }, -} - -// ── sig-003: STAGE_3_ENRICHED ───────────────────────────────────────────────── -const sig003Pipeline: PipelineState = { - signalId: 'sig-003', - currentStage: PipelineStage.STAGE_3_ENRICHED, - overallEligible: false, - publishedToFutureAvailability: false, - gates: { - EVIDENCE_GATE: gate( - GateType.EVIDENCE_GATE, - GateStatus.PASSED, - 'LinkedIn-Datenauszug als Evidenz vorhanden.', - [ - { label: 'Evidenz vorhanden', passed: true, value: '1 Stück' }, - { label: 'Quelle angegeben', passed: true, value: 'LinkedIn Hiring Data' }, - { label: 'Inhalt nicht leer', passed: true }, - ], - ), - CONFIDENCE_GATE: gate( - GateType.CONFIDENCE_GATE, - GateStatus.PENDING, - 'Konfidenzwert 0.61 liegt im Grenzbereich (0.60–0.65). Zusätzliche Validierung empfohlen.', - [ - { label: 'Konfidenz ≥ 0.60', passed: true, value: '0.61', note: 'Grenzbereich' }, - { label: 'Konfidenz ≥ 0.75 (hoch)', passed: false, value: '0.61' }, - ], - 'Unternehmensidentität über HR-Netzwerke verifizieren, um Konfidenz zu erhöhen.', - ), - SENSITIVITY_GATE: gate( - GateType.SENSITIVITY_GATE, - GateStatus.PASSED, - 'Öffentliche Quelle – keine Einschränkungen.', - [ - { label: 'Nicht CONFIDENTIAL', passed: true, value: 'PUBLIC' }, - { label: 'Nicht RESTRICTED', passed: true }, - ], - ), - REVIEW_GATE: gate( - GateType.REVIEW_GATE, - GateStatus.PENDING, - 'Signal noch nicht für Review eingereicht.', - [ - { label: 'Review angefordert', passed: false }, - { label: 'Review abgeschlossen', passed: false }, - ], - 'Signal für Analyst-Review einreichen nach Konfidenz-Verbesserung.', - ), - MATCHABILITY_GATE: gate( - GateType.MATCHABILITY_GATE, - GateStatus.PENDING, - 'Matchbarkeit noch nicht bewertet – Anreicherung läuft.', - [ - { label: 'Asset-Typ definiert', passed: true, value: 'OFFICE' }, - { label: 'Standort definiert', passed: true, value: 'Zug' }, - { label: 'Zeithorizont definiert', passed: false }, - { label: 'Geschäftsrelevanz vorhanden', passed: false, note: 'Unternehmensname unbekannt' }, - ], - ), - FEED_ELIGIBILITY_GATE: gate( - GateType.FEED_ELIGIBILITY_GATE, - GateStatus.BLOCKED, - 'Feed-Eignung blockiert: Mehrere vorgelagerte Gates ausstehend.', - [ - { label: 'Review-Gate bestanden', passed: false }, - { label: 'Matchbarkeits-Gate bestanden', passed: false }, - ], - ), - }, -} - -// ── sig-004: STAGE_4_REVIEW_CANDIDATE ───────────────────────────────────────── -const sig004Pipeline: PipelineState = { - signalId: 'sig-004', - currentStage: PipelineStage.STAGE_4_REVIEW_CANDIDATE, - overallEligible: false, - publishedToFutureAvailability: false, - gates: { - EVIDENCE_GATE: gate( - GateType.EVIDENCE_GATE, - GateStatus.PASSED, - 'Mietvertragsdokument als primäre Evidenz vorhanden.', - [ - { label: 'Evidenz vorhanden', passed: true, value: '1 Stück' }, - { label: 'Quelle angegeben', passed: true, value: 'Internes ERP' }, - { label: 'Inhalt nicht leer', passed: true }, - ], - ), - CONFIDENCE_GATE: gate( - GateType.CONFIDENCE_GATE, - GateStatus.PASSED, - 'Konfidenzwert 0.97 – sehr hohe Verlässlichkeit (interne Vertragsdaten).', - [ - { label: 'Konfidenz ≥ 0.60', passed: true, value: '0.97' }, - { label: 'Konfidenz ≥ 0.75 (hoch)', passed: true, value: '0.97' }, - ], - ), - SENSITIVITY_GATE: gate( - GateType.SENSITIVITY_GATE, - GateStatus.FAILED, - 'Sensitivitätsstufe CONFIDENTIAL: Signal darf nicht im Demand Feed erscheinen. Mieteridentität ist schützenswert.', - [ - { label: 'Nicht CONFIDENTIAL', passed: false, value: 'CONFIDENTIAL', note: 'Interne Vertragsdaten – nur für Property Manager' }, - { label: 'Nicht RESTRICTED', passed: true }, - ], - 'Daten anonymisieren oder auf aggregierter Ebene veröffentlichen.', - ), - REVIEW_GATE: gate( - GateType.REVIEW_GATE, - GateStatus.PENDING, - 'Review angefordert – Sensitivitäts-Gate muss zuerst adressiert werden.', - [ - { label: 'Review angefordert', passed: true }, - { label: 'Review abgeschlossen', passed: false }, - ], - 'Sensitivitätsproblem lösen, dann Review abschliessen.', - ), - MATCHABILITY_GATE: gate( - GateType.MATCHABILITY_GATE, - GateStatus.PENDING, - 'Matchbarkeit ausstehend – zuerst Sensitivitätsproblem lösen.', - [ - { label: 'Asset-Typ definiert', passed: true, value: 'OFFICE' }, - { label: 'Standort definiert', passed: true, value: 'Zürich-West' }, - { label: 'Zeithorizont definiert', passed: true, value: '03/2026' }, - { label: 'Geschäftsrelevanz vorhanden', passed: true }, - ], - ), - FEED_ELIGIBILITY_GATE: gate( - GateType.FEED_ELIGIBILITY_GATE, - GateStatus.BLOCKED, - 'Feed-Eignung blockiert: Sensitivitäts-Gate fehlgeschlagen.', - [ - { label: 'Sensitivitäts-Gate bestanden', passed: false, value: 'CONFIDENTIAL' }, - { label: 'Review-Gate bestanden', passed: false }, - ], - 'Anonymisierung der Mieterdaten durchführen.', - ), - }, -} - -// ── sig-005: STAGE_2_NORMALIZED ─────────────────────────────────────────────── -const sig005Pipeline: PipelineState = { - signalId: 'sig-005', - currentStage: PipelineStage.STAGE_2_NORMALIZED, - overallEligible: false, - publishedToFutureAvailability: false, - gates: { - EVIDENCE_GATE: gate( - GateType.EVIDENCE_GATE, - GateStatus.PASSED, - 'Handelsregistermutation als verlässliche Primärquelle.', - [ - { label: 'Evidenz vorhanden', passed: true, value: '1 Stück' }, - { label: 'Quelle angegeben', passed: true, value: 'Zefix – Handelsregister Schweiz' }, - { label: 'Inhalt nicht leer', passed: true }, - ], - ), - CONFIDENCE_GATE: gate( - GateType.CONFIDENCE_GATE, - GateStatus.PENDING, - 'Konfidenzwert 0.63 – Sitzverlegung bestätigt, aber Flächenbedarf am Zielort noch nicht validiert.', - [ - { label: 'Konfidenz ≥ 0.60', passed: true, value: '0.63', note: 'Grenzbereich' }, - { label: 'Konfidenz ≥ 0.75 (hoch)', passed: false, value: '0.63' }, - ], - 'Flächenbedarf in Kloten durch Direktkontakt oder weitere Quellenrecherche bestätigen.', - ), - SENSITIVITY_GATE: gate( - GateType.SENSITIVITY_GATE, - GateStatus.PASSED, - 'Öffentliches Handelsregister – keine Sensitivitätsbedenken.', - [ - { label: 'Nicht CONFIDENTIAL', passed: true, value: 'PUBLIC' }, - { label: 'Nicht RESTRICTED', passed: true }, - ], - ), - REVIEW_GATE: gate( - GateType.REVIEW_GATE, - GateStatus.PENDING, - 'Signal noch in Normalisierungsphase – kein Review angefordert.', - [ - { label: 'Review angefordert', passed: false }, - { label: 'Review abgeschlossen', passed: false }, - ], - 'Anreicherung abschliessen, dann für Review einreichen.', - ), - MATCHABILITY_GATE: gate( - GateType.MATCHABILITY_GATE, - GateStatus.PENDING, - 'Anreicherung noch nicht abgeschlossen.', - [ - { label: 'Asset-Typ definiert', passed: true, value: 'LOGISTICS' }, - { label: 'Standort definiert', passed: true, value: 'Kloten' }, - { label: 'Zeithorizont definiert', passed: false }, - { label: 'Geschäftsrelevanz vorhanden', passed: false }, - ], - ), - FEED_ELIGIBILITY_GATE: gate( - GateType.FEED_ELIGIBILITY_GATE, - GateStatus.BLOCKED, - 'Feed-Eignung blockiert: Signal befindet sich noch in der Normalisierungsphase.', - [ - { label: 'Review-Gate bestanden', passed: false }, - { label: 'Matchbarkeits-Gate bestanden', passed: false }, - ], - ), - }, -} - -// ── sig-006: STAGE_1_RAW_EVIDENCE ──────────────────────────────────────────── -const sig006Pipeline: PipelineState = { - signalId: 'sig-006', - currentStage: PipelineStage.STAGE_1_RAW_EVIDENCE, - overallEligible: false, - publishedToFutureAvailability: false, - gates: { - EVIDENCE_GATE: gate( - GateType.EVIDENCE_GATE, - GateStatus.PASSED, - 'Inserat auf Immoscout24 als Primärquelle vorhanden.', - [ - { label: 'Evidenz vorhanden', passed: true, value: '1 Stück' }, - { label: 'Quelle angegeben', passed: true, value: 'Immoscout24' }, - { label: 'Inhalt nicht leer', passed: true }, - ], - ), - CONFIDENCE_GATE: gate( - GateType.CONFIDENCE_GATE, - GateStatus.PASSED, - 'Konfidenzwert 0.83 – direkte Nachfrage-Ausschreibung mit hoher Relevanz.', - [ - { label: 'Konfidenz ≥ 0.60', passed: true, value: '0.83' }, - { label: 'Konfidenz ≥ 0.75 (hoch)', passed: true, value: '0.83' }, - ], - ), - SENSITIVITY_GATE: gate( - GateType.SENSITIVITY_GATE, - GateStatus.PASSED, - 'Öffentliche Plattform – keine Einschränkungen.', - [ - { label: 'Nicht CONFIDENTIAL', passed: true, value: 'PUBLIC' }, - { label: 'Nicht RESTRICTED', passed: true }, - ], - ), - REVIEW_GATE: gate( - GateType.REVIEW_GATE, - GateStatus.PENDING, - 'Signal soeben erkannt – Normalisierung und Anreicherung noch ausstehend.', - [ - { label: 'Review angefordert', passed: false }, - { label: 'Review abgeschlossen', passed: false }, - ], - 'Signal normalisieren und anreichern, dann für Review einreichen.', - ), - MATCHABILITY_GATE: gate( - GateType.MATCHABILITY_GATE, - GateStatus.PENDING, - 'Matchbarkeit noch nicht bewertet – Signal in früher Phase.', - [ - { label: 'Asset-Typ definiert', passed: true, value: 'LOGISTICS' }, - { label: 'Standort definiert', passed: true, value: 'Winterthur' }, - { label: 'Zeithorizont definiert', passed: true, value: 'September 2025' }, - { label: 'Geschäftsrelevanz vorhanden', passed: false }, - ], - ), - FEED_ELIGIBILITY_GATE: gate( - GateType.FEED_ELIGIBILITY_GATE, - GateStatus.BLOCKED, - 'Feed-Eignung blockiert: Signal in Rohphase – mehrere Stufen offen.', - [ - { label: 'Review-Gate bestanden', passed: false }, - { label: 'Matchbarkeits-Gate bestanden', passed: false }, - ], - ), - }, -} - -// ── sig-007: STAGE_3_ENRICHED ───────────────────────────────────────────────── -const sig007Pipeline: PipelineState = { - signalId: 'sig-007', - currentStage: PipelineStage.STAGE_3_ENRICHED, - overallEligible: false, - publishedToFutureAvailability: false, - gates: { - EVIDENCE_GATE: gate( - GateType.EVIDENCE_GATE, - GateStatus.PASSED, - 'SBB-Medienmitteilung als verlässliche behördliche Quelle.', - [ - { label: 'Evidenz vorhanden', passed: true, value: '1 Stück' }, - { label: 'Quelle angegeben', passed: true, value: 'SBB Medienmitteilung' }, - { label: 'Inhalt nicht leer', passed: true }, - ], - ), - CONFIDENCE_GATE: gate( - GateType.CONFIDENCE_GATE, - GateStatus.PASSED, - 'Konfidenzwert 0.79 – behördliche Bestätigung vorhanden.', - [ - { label: 'Konfidenz ≥ 0.60', passed: true, value: '0.79' }, - { label: 'Konfidenz ≥ 0.75 (hoch)', passed: true, value: '0.79' }, - ], - ), - SENSITIVITY_GATE: gate( - GateType.SENSITIVITY_GATE, - GateStatus.PASSED, - 'Öffentliche SBB-Mitteilung – keine Sensitivitätsbedenken.', - [ - { label: 'Nicht CONFIDENTIAL', passed: true, value: 'PUBLIC' }, - { label: 'Nicht RESTRICTED', passed: true }, - ], - ), - REVIEW_GATE: gate( - GateType.REVIEW_GATE, - GateStatus.PENDING, - 'Matchbarkeits-Problem muss zuerst gelöst werden.', - [ - { label: 'Review angefordert', passed: false }, - { label: 'Review abgeschlossen', passed: false }, - ], - 'Matchbarkeits-Frage klären (kein direktes Verfügbarkeitssignal), dann Review starten.', - ), - MATCHABILITY_GATE: gate( - GateType.MATCHABILITY_GATE, - GateStatus.FAILED, - 'Signal Typ PROJECT_DEVELOPMENT ohne direktes Verfügbarkeitssignal – kein konkreter Flächenbedarf erkennbar.', - [ - { label: 'Asset-Typ definiert', passed: true, value: 'OFFICE, PRODUCTION' }, - { label: 'Standort definiert', passed: true, value: 'Schlieren' }, - { label: 'Zeithorizont definiert', passed: true, value: 'Dezember 2027' }, - { label: 'Direktes Verfügbarkeitssignal', passed: false, note: 'Infrastrukturverbesserung ohne konkreten Flächenbedarf' }, - ], - 'Signal als strategischen Kontext-Input klassifizieren oder konkreten Bedarf nachweisen.', - ), - FEED_ELIGIBILITY_GATE: gate( - GateType.FEED_ELIGIBILITY_GATE, - GateStatus.BLOCKED, - 'Feed-Eignung blockiert: Matchbarkeits-Gate fehlgeschlagen.', - [ - { label: 'Matchbarkeits-Gate bestanden', passed: false }, - { label: 'Review-Gate bestanden', passed: false }, - ], - 'Signal als strategischen Input behandeln oder Matchbarkeit nachweisen.', - ), - }, -} - -// ── sig-008: STAGE_6_MATCHABLE_RESULT ──────────────────────────────────────── -const sig008Pipeline: PipelineState = { - signalId: 'sig-008', - currentStage: PipelineStage.STAGE_6_MATCHABLE_RESULT, - overallEligible: true, - publishedToFutureAvailability: true, - publishedAt: '2026-05-13T11:00:00Z', - feedDisclaimer: 'Nur in aggregierter Form – Mieteridentität anonymisiert. Kein Rückschluss auf Vertragsparteien möglich.', - gates: { - EVIDENCE_GATE: gate( - GateType.EVIDENCE_GATE, - GateStatus.PASSED, - 'Eigene Marktbeobachtung mit Vor-Ort-Begehung dokumentiert.', - [ - { label: 'Evidenz vorhanden', passed: true, value: '1 Stück' }, - { label: 'Quelle angegeben', passed: true, value: 'Eigene Beobachtung – Marktanalyse' }, - { label: 'Inhalt nicht leer', passed: true }, - ], - ), - CONFIDENCE_GATE: gate( - GateType.CONFIDENCE_GATE, - GateStatus.PASSED, - 'Konfidenzwert 0.68 – ausreichend für interne Verarbeitung.', - [ - { label: 'Konfidenz ≥ 0.60', passed: true, value: '0.68' }, - { label: 'Konfidenz ≥ 0.75 (hoch)', passed: false, value: '0.68', note: 'Mittlere Konfidenz akzeptiert' }, - ], - ), - SENSITIVITY_GATE: gate( - GateType.SENSITIVITY_GATE, - GateStatus.PASSED, - 'Sensitivitätsstufe INTERNAL mit Anonymisierungsauflage zulässig.', - [ - { label: 'Nicht CONFIDENTIAL', passed: true, value: 'INTERNAL' }, - { label: 'Nicht RESTRICTED', passed: true }, - { label: 'Anonymisierungsauflage gesetzt', passed: true }, - ], - ), - REVIEW_GATE: gate( - GateType.REVIEW_GATE, - GateStatus.PASSED, - 'Signal durch Analyst A. Müller am 13.05.2026 genehmigt und zur Publikation freigegeben.', - [ - { label: 'Review angefordert', passed: true }, - { label: 'Review abgeschlossen', passed: true, value: 'A. Müller, 13.05.2026' }, - { label: 'Genehmigt', passed: true }, - ], - ), - MATCHABILITY_GATE: gate( - GateType.MATCHABILITY_GATE, - GateStatus.PASSED, - 'Alle notwendigen Felder für Match-Engine vorhanden.', - [ - { label: 'Asset-Typ definiert', passed: true, value: 'OFFICE' }, - { label: 'Standort definiert', passed: true, value: 'Lausanne' }, - { label: 'Zeithorizont definiert', passed: true, value: 'Sofort verfügbar (3+ Monate leer)' }, - { label: 'Geschäftsrelevanz vorhanden', passed: true }, - ], - ), - FEED_ELIGIBILITY_GATE: gate( - GateType.FEED_ELIGIBILITY_GATE, - GateStatus.PASSED, - 'Signal im Future Availability Feed publiziert – anonymisiert und aggregiert.', - [ - { label: 'Review-Gate bestanden', passed: true }, - { label: 'Matchbarkeits-Gate bestanden', passed: true }, - { label: 'Anonymisierungsauflage umgesetzt', passed: true }, - { label: 'Im Feed publiziert', passed: true, value: '13.05.2026' }, - ], - ), - }, -} - -export const MOCK_PIPELINE_STATES: PipelineState[] = [ - sig001Pipeline, - sig002Pipeline, - sig003Pipeline, - sig004Pipeline, - sig005Pipeline, - sig006Pipeline, - sig007Pipeline, - sig008Pipeline, -] - -// ── Audit Trails ────────────────────────────────────────────────────────────── - -export const MOCK_AUDIT_TRAILS: AuditTrailEntry[] = [ - // sig-001 - { - id: 'audit-001-1', - signalId: 'sig-001', - timestamp: '2025-05-10T09:23:00Z', - stage: PipelineStage.STAGE_1_RAW_EVIDENCE, - action: 'Signal erkannt', - performedBy: 'System / NZZ-Connector', - details: 'Signal automatisch aus NZZ-Medienbericht extrahiert. Rohtext gespeichert.', - }, - { - id: 'audit-001-2', - signalId: 'sig-001', - timestamp: '2025-05-10T10:05:00Z', - stage: PipelineStage.STAGE_2_NORMALIZED, - action: 'Normalisierung abgeschlossen', - performedBy: 'System / NLP-Pipeline', - details: 'Felder normalisiert: Standort "Zürich ZH", Asset-Typ OFFICE, Signaltyp EXPANSION.', - }, - { - id: 'audit-001-3', - signalId: 'sig-001', - timestamp: '2025-05-11T08:00:00Z', - stage: PipelineStage.STAGE_3_ENRICHED, - action: 'Anreicherung abgeschlossen', - performedBy: 'System / Enrichment-Engine', - details: 'Entitäten extrahiert: UBS AG (Konfidenz 0.98), Zürich (0.95), Ende 2025 (0.87). Konfidenzwert: 0.72.', - }, - { - id: 'audit-001-4', - signalId: 'sig-001', - timestamp: '2025-05-11T08:05:00Z', - stage: PipelineStage.STAGE_4_REVIEW_CANDIDATE, - action: 'Zur Prüfung eingereicht', - performedBy: 'System / Gate-Evaluator', - details: 'Evidenz-Gate, Konfidenz-Gate und Sensitivitäts-Gate bestanden. Review-Gate offen – wartet auf Analyst.', - gateType: GateType.REVIEW_GATE, - }, - - // sig-002 - { - id: 'audit-002-1', - signalId: 'sig-002', - timestamp: '2025-05-08T14:00:00Z', - stage: PipelineStage.STAGE_1_RAW_EVIDENCE, - action: 'Signal erkannt', - performedBy: 'System / Baugesuch-Connector', - details: 'Baugesuch Nr. BS-2025-0312 aus öffentlichem Register extrahiert.', - }, - { - id: 'audit-002-2', - signalId: 'sig-002', - timestamp: '2025-05-08T15:00:00Z', - stage: PipelineStage.STAGE_3_ENRICHED, - action: 'Normalisierung und Anreicherung', - performedBy: 'System / NLP-Pipeline', - details: 'Alle Felder normalisiert und angereichert. Standort Basel-Nord, Asset-Typen: OFFICE, LOGISTICS, RETAIL. Konfidenz: 0.88.', - }, - { - id: 'audit-002-3', - signalId: 'sig-002', - timestamp: '2025-05-12T09:30:00Z', - stage: PipelineStage.STAGE_4_REVIEW_CANDIDATE, - action: 'Analyst-Review gestartet', - performedBy: 'M. Huber', - details: 'Review-Kandidat angenommen. Öffentliche Quelle mit hoher Verlässlichkeit.', - }, - { - id: 'audit-002-4', - signalId: 'sig-002', - timestamp: '2025-05-12T09:45:00Z', - stage: PipelineStage.STAGE_5_APPROVED_FUTURE, - action: 'Signal genehmigt', - performedBy: 'M. Huber', - details: 'Signal als Future Availability genehmigt. Alle Gates bestanden. Bereit zur Feed-Publikation.', - gateType: GateType.REVIEW_GATE, - }, - - // sig-003 - { - id: 'audit-003-1', - signalId: 'sig-003', - timestamp: '2025-05-09T11:30:00Z', - stage: PipelineStage.STAGE_1_RAW_EVIDENCE, - action: 'Signal erkannt', - performedBy: 'System / LinkedIn-Connector', - details: 'Stellenwachstumssignal aus LinkedIn-Daten extrahiert. 80 neue Stellenausschreibungen in 6 Monaten.', - }, - { - id: 'audit-003-2', - signalId: 'sig-003', - timestamp: '2025-05-09T12:00:00Z', - stage: PipelineStage.STAGE_2_NORMALIZED, - action: 'Normalisierung abgeschlossen', - performedBy: 'System / NLP-Pipeline', - details: 'Standort Zug, Asset-Typ OFFICE, Signaltyp EXPANSION normalisiert. Unternehmensname nicht öffentlich.', - }, - { - id: 'audit-003-3', - signalId: 'sig-003', - timestamp: '2025-05-11T14:00:00Z', - stage: PipelineStage.STAGE_3_ENRICHED, - action: 'Anreicherung abgeschlossen', - performedBy: 'System / Enrichment-Engine', - details: 'Konfidenzwert 0.61 – Grenzbereich. Unternehmensverifizierung empfohlen. Weiterer Review nötig.', - }, - - // sig-004 - { - id: 'audit-004-1', - signalId: 'sig-004', - timestamp: '2025-05-05T08:00:00Z', - stage: PipelineStage.STAGE_1_RAW_EVIDENCE, - action: 'Signal erkannt', - performedBy: 'System / ERP-Connector', - details: 'Vertragslaufdaten aus internem ERP importiert. Vertrag V-2019-0481, Ablauf 31.03.2026.', - }, - { - id: 'audit-004-2', - signalId: 'sig-004', - timestamp: '2025-05-05T08:30:00Z', - stage: PipelineStage.STAGE_3_ENRICHED, - action: 'Normalisierung und Anreicherung', - performedBy: 'System / NLP-Pipeline', - details: 'Alle Felder normalisiert. Hohe Konfidenz (0.97) da Primärquelle. Sensitivität CONFIDENTIAL erkannt.', - }, - { - id: 'audit-004-3', - signalId: 'sig-004', - timestamp: '2025-05-10T10:00:00Z', - stage: PipelineStage.STAGE_4_REVIEW_CANDIDATE, - action: 'Sensitivitäts-Warnung gesetzt', - performedBy: 'System / Gate-Evaluator', - details: 'Sensitivitäts-Gate fehlgeschlagen: CONFIDENTIAL. Signal zur Review eingereicht mit Anonymisierungsauflage.', - gateType: GateType.SENSITIVITY_GATE, - }, - { - id: 'audit-004-4', - signalId: 'sig-004', - timestamp: '2025-05-10T10:05:00Z', - stage: PipelineStage.STAGE_4_REVIEW_CANDIDATE, - action: 'Property Manager benachrichtigt', - performedBy: 'System / Notification-Service', - details: 'Property Manager über auslaufenden Grossmietvertrag informiert. Verlängerungsgespräch empfohlen.', - }, - - // sig-005 - { - id: 'audit-005-1', - signalId: 'sig-005', - timestamp: '2025-05-07T16:20:00Z', - stage: PipelineStage.STAGE_1_RAW_EVIDENCE, - action: 'Signal erkannt', - performedBy: 'System / Handelsregister-Connector', - details: 'Sitzverlegung aus Zefix extrahiert: 3014 Bern → 8302 Kloten, eingetragen 07.05.2025.', - }, - { - id: 'audit-005-2', - signalId: 'sig-005', - timestamp: '2025-05-07T17:00:00Z', - stage: PipelineStage.STAGE_2_NORMALIZED, - action: 'Normalisierung abgeschlossen', - performedBy: 'System / NLP-Pipeline', - details: 'Standort Kloten (Zielort), Asset-Typ LOGISTICS normalisiert. Flächenbedarf noch nicht validiert.', - }, - { - id: 'audit-005-3', - signalId: 'sig-005', - timestamp: '2025-05-08T09:00:00Z', - stage: PipelineStage.STAGE_2_NORMALIZED, - action: 'Anreicherung gestartet', - performedBy: 'System / Enrichment-Engine', - details: 'Konfidenzwert 0.63 – Sitzverlegung bestätigt, konkreter Flächenbedarf am Zielort ausstehend.', - }, - - // sig-006 - { - id: 'audit-006-1', - signalId: 'sig-006', - timestamp: '2025-05-12T08:15:00Z', - stage: PipelineStage.STAGE_1_RAW_EVIDENCE, - action: 'Signal erkannt', - performedBy: 'System / Immoscout-Connector', - details: 'Neue Suchanzeige auf Immoscout24 erkannt: 800 m² Lagerfläche Winterthur, sofort gesucht.', - }, - { - id: 'audit-006-2', - signalId: 'sig-006', - timestamp: '2025-05-12T08:30:00Z', - stage: PipelineStage.STAGE_1_RAW_EVIDENCE, - action: 'Evidenz-Gate bestanden', - performedBy: 'System / Gate-Evaluator', - details: 'Evidenz-Gate und Konfidenz-Gate (0.83) bestanden. Signal in Pipeline aufgenommen, Normalisierung startet.', - gateType: GateType.EVIDENCE_GATE, - }, - - // sig-007 - { - id: 'audit-007-1', - signalId: 'sig-007', - timestamp: '2025-05-06T10:00:00Z', - stage: PipelineStage.STAGE_1_RAW_EVIDENCE, - action: 'Signal erkannt', - performedBy: 'System / SBB-Feed-Connector', - details: 'SBB-Medienmitteilung zur neuen Haltestelle Schlieren-West 2027 automatisch verarbeitet.', - }, - { - id: 'audit-007-2', - signalId: 'sig-007', - timestamp: '2025-05-06T11:00:00Z', - stage: PipelineStage.STAGE_2_NORMALIZED, - action: 'Normalisierung abgeschlossen', - performedBy: 'System / NLP-Pipeline', - details: 'Standort Schlieren, Asset-Typen OFFICE und PRODUCTION, Zeithorizont Dezember 2027 normalisiert.', - }, - { - id: 'audit-007-3', - signalId: 'sig-007', - timestamp: '2025-05-10T15:00:00Z', - stage: PipelineStage.STAGE_3_ENRICHED, - action: 'Matchbarkeits-Problem identifiziert', - performedBy: 'System / Gate-Evaluator', - details: 'Matchbarkeits-Gate fehlgeschlagen: PROJECT_DEVELOPMENT ohne direktes Verfügbarkeitssignal. Kein konkreter Flächenbedarf nachweisbar.', - gateType: GateType.MATCHABILITY_GATE, - }, - { - id: 'audit-007-4', - signalId: 'sig-007', - timestamp: '2025-05-10T15:05:00Z', - stage: PipelineStage.STAGE_3_ENRICHED, - action: 'Analyst-Notiz hinzugefügt', - performedBy: 'K. Bauer', - details: 'Infrastrukturverbesserung dokumentiert. Empfehlung: Als strategischen Kontext-Input behandeln, nicht als direktes Matchingobjekt.', - }, - - // sig-008 - { - id: 'audit-008-1', - signalId: 'sig-008', - timestamp: '2025-05-03T14:30:00Z', - stage: PipelineStage.STAGE_1_RAW_EVIDENCE, - action: 'Signal manuell erfasst', - performedBy: 'A. Müller', - details: 'Vor-Ort-Begehung Tour de Berne, Lausanne. 4. OG leer stehend, keine Vermarktung erkennbar.', - }, - { - id: 'audit-008-2', - signalId: 'sig-008', - timestamp: '2025-05-03T15:00:00Z', - stage: PipelineStage.STAGE_3_ENRICHED, - action: 'Normalisierung und Anreicherung', - performedBy: 'System / NLP-Pipeline', - details: 'Standort Lausanne, Asset-Typ OFFICE, Signaltyp POSSIBLE_MOVE_OUT normalisiert. Konfidenz 0.68.', - }, - { - id: 'audit-008-3', - signalId: 'sig-008', - timestamp: '2025-05-13T10:30:00Z', - stage: PipelineStage.STAGE_4_REVIEW_CANDIDATE, - action: 'Analyst-Review abgeschlossen', - performedBy: 'A. Müller', - details: 'Signal geprüft und genehmigt. Anonymisierungsauflage gesetzt: Mieteridentität darf nicht kommuniziert werden.', - gateType: GateType.REVIEW_GATE, - }, - { - id: 'audit-008-4', - signalId: 'sig-008', - timestamp: '2025-05-13T11:00:00Z', - stage: PipelineStage.STAGE_6_MATCHABLE_RESULT, - action: 'In Future Availability publiziert', - performedBy: 'A. Müller', - details: 'Signal als Future Availability "fs-lausanne-001" in den Feed übertragen. Anonymisierter Disclaimer aktiv.', - gateType: GateType.FEED_ELIGIBILITY_GATE, - }, -] diff --git a/.claude/worktrees/agent-a82a3716/src/pages/Home.tsx b/.claude/worktrees/agent-a82a3716/src/pages/Home.tsx deleted file mode 100644 index 27f0036..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/Home.tsx +++ /dev/null @@ -1,43 +0,0 @@ -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/.claude/worktrees/agent-a82a3716/src/pages/auth/LoginScreen.tsx b/.claude/worktrees/agent-a82a3716/src/pages/auth/LoginScreen.tsx deleted file mode 100644 index c01eb4f..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/auth/LoginScreen.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { useState } from 'react' -import { useNavigate, Navigate } from 'react-router' -import { - Box, - Button, - Card, - CardContent, - Chip, - CircularProgress, - Divider, - TextField, - Typography, -} from '@mui/material' -import { Building2 } from 'lucide-react' -import { authService } from '../../services/authService' -import { useSessionStore } from '../../stores/sessionStore' -import { UserRole } from '../../domain/enums' - -const DEMO_ROLES: { role: UserRole; label: string; description: string }[] = [ - { role: UserRole.ORGANIZATION_ADMIN, label: 'Org Admin', description: 'Vollzugriff Supply + Demand + Ops' }, - { role: UserRole.PROPERTY_MANAGER, label: 'Verwaltung', description: 'Portfolio verwalten + Markt durchsuchen' }, - { role: UserRole.DEMAND_USER, label: 'Bürosuche', description: 'Nur Marktsuche — kein Portfolio' }, - { role: UserRole.REVIEWER, label: 'Reviewer', description: 'Operations Workspace' }, - { role: UserRole.OWNER_VIEWER, label: 'Eigentümer', description: 'Supply (eingeschränkt)' }, - { role: UserRole.SUPER_ADMIN, label: 'Super Admin', description: 'Plattform-Administrator' }, -] - -export default function LoginScreen() { - const { isAuthenticated } = useSessionStore() - const navigate = useNavigate() - const [email, setEmail] = useState('admin@ideal-sharing.ch') - const [password, setPassword] = useState('') - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - if (isAuthenticated) { - return - } - - async function handleLogin(e: React.FormEvent) { - e.preventDefault() - if (!email) { - setError('Bitte E-Mail-Adresse eingeben.') - return - } - setLoading(true) - setError(null) - try { - await authService.login(email, password) - navigate('/') - } catch { - setError('Anmeldung fehlgeschlagen. Bitte erneut versuchen.') - } finally { - setLoading(false) - } - } - - async function handleDemoLogin(role: UserRole) { - setLoading(true) - try { - await authService.switchDemoRole(role) - navigate('/') - } finally { - setLoading(false) - } - } - - return ( - - - {/* Branding */} - - - - - - - Property Match - - - Decision Intelligence - - - - - {/* Login card */} - - - Anmelden - - Melden Sie sich mit Ihren Zugangsdaten an. - - - - setEmail(e.target.value)} - autoComplete="email" - required - /> - setPassword(e.target.value)} - autoComplete="current-password" - helperText="Im Demo-Modus wird jedes Passwort akzeptiert." - /> - - {error && ( - - {error} - - )} - - - - - - - {/* Demo access */} - - - - - - Demo-Zugänge - - - - - - - {DEMO_ROLES.map(({ role, label, description }) => ( - handleDemoLogin(role)} - sx={{ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - px: 1.5, - py: 1, - borderRadius: 1, - border: '1px solid #e2e8f0', - cursor: 'pointer', - transition: 'border-color 0.15s', - '&:hover': { borderColor: '#1e3a5f', bgcolor: 'rgba(30,58,95,0.03)' }, - }} - > - - - {label} - - - {description} - - - - - ))} - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/demand/AISearch.tsx b/.claude/worktrees/agent-a82a3716/src/pages/demand/AISearch.tsx deleted file mode 100644 index 0de7f3f..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/demand/AISearch.tsx +++ /dev/null @@ -1,390 +0,0 @@ -import { useRef, useState } from 'react' -import { - Alert, - Box, - Button, - CircularProgress, - Divider, - Typography, -} from '@mui/material' -import { ArrowRight, Bookmark, Save, Search } from 'lucide-react' -import { useNavigate } from 'react-router' -import { useQueryClient } from '@tanstack/react-query' -import { - NeedBuilderProgress, - NeedInput, - VoiceNeedInput, - WeightingEditor, - NeedCardPreview, - NeedBuilderErrorState, -} from '../../components/demand' -import { aiService } from '../../services/aiService' -import { needService } from '../../services/needService' -import { weightingService } from '../../services/weightingService' -import { NeedBuilderStep } from '../../domain/needBuilder' -import type { ParseNeedResult, ParsedNeedCriteria, WeightingKey } from '../../domain/needBuilder' -import { AssetType } from '../../domain/enums' -import type { CreateNeedInput } from '../../domain/need' - -// ── Helpers ─────────────────────────────────────────────────────────────────── - -const ASSET_LABELS_TEXT: Record = { - OFFICE: 'Bürofläche', RETAIL: 'Retail-Fläche', LOGISTICS: 'Logistikfläche', - PRODUCTION: 'Produktionsfläche', LIGHT_INDUSTRIAL: 'Gewerbefläche', MIXED: 'gemischte Fläche', -} - -function generateSummary(c: ParsedNeedCriteria): string { - const parts: string[] = [] - if (c.assetType) parts.push(`Suche ${ASSET_LABELS_TEXT[c.assetType] ?? c.assetType}`) - if (c.areaRange && (c.areaRange.min > 0 || c.areaRange.max > 0)) - parts.push(`${c.areaRange.min}–${c.areaRange.max} m²`) - if (c.preferredLocations?.length) parts.push(`in ${c.preferredLocations.join(', ')}`) - if (c.budgetRange?.maxPerSqm) parts.push(`Budget max. CHF ${c.budgetRange.maxPerSqm}/m²`) - if (c.timing?.earliestMoveIn) parts.push(`ab ${c.timing.earliestMoveIn}`) - if (c.mustHaveCriteria?.length) parts.push(`Must-haves: ${c.mustHaveCriteria.join(', ')}`) - return parts.join(', ') -} - -function buildNeedInput( - criteria: ParsedNeedCriteria, - weights: Record, - needTitle: string, - overallConfidence: number, - status: 'DRAFT' | 'ACTIVE', -): CreateNeedInput { - return { - companyName: needTitle || criteria.companyName || 'Neue Suche', - assetType: criteria.assetType ?? AssetType.UNKNOWN, - requiredArea: criteria.areaRange ?? { min: 0, max: 0 }, - preferredLocations: criteria.preferredLocations ?? [], - budgetRange: criteria.budgetRange ?? { maxPerSqm: 0, currency: 'CHF' }, - timing: { - earliestMoveIn: criteria.timing?.earliestMoveIn ?? '', - latestMoveIn: criteria.timing?.latestMoveIn ?? criteria.timing?.earliestMoveIn ?? '', - contractDurationMonths: criteria.timing?.contractDurationMonths, - flexibleTiming: criteria.timing?.flexibleTiming ?? true, - }, - weightingProfile: weights, - confidenceInCriteria: overallConfidence, - status, - mustCriteriaText: criteria.mustHaveCriteria ?? [], - notes: criteria.notes, - extractedFromText: undefined, - } -} - -// ── Action intent ───────────────────────────────────────────────────────────── - -type ActionIntent = 'search' | 'save-profile' - -// ── Page ────────────────────────────────────────────────────────────────────── - -export default function AISearch() { - const navigate = useNavigate() - const queryClient = useQueryClient() - - const [step, setStep] = useState(NeedBuilderStep.IDLE) - const [intent, setIntent] = useState('search') - const [inputText, setInputText] = useState('') - const [isAutoGen, setIsAutoGen] = useState(false) - const [criteria, setCriteria] = useState({}) - const [parseResult, setParseResult] = useState(null) - const [editedCriteria, setEditedCriteria] = useState(null) - const [weights, setWeights] = useState>(weightingService.getDefaultWeights()) - const [weightingKey, setWeightingKey] = useState(0) - const [needTitle, setNeedTitle] = useState('') - const [error, setError] = useState(null) - - const isManualTextRef = useRef(false) - - function handleCriteriaChange(next: ParsedNeedCriteria) { - setCriteria(next) - if (!isManualTextRef.current) { - const summary = generateSummary(next) - setInputText(summary) - setIsAutoGen(!!summary) - } - } - - function handleTextChange(text: string) { - isManualTextRef.current = text !== '' - setIsAutoGen(false) - setInputText(text) - } - - async function handleAiAutofill() { - setStep(NeedBuilderStep.PARSING) - setError(null) - try { - const resp = await aiService.parseNeed(inputText) - const result = resp.data - setParseResult(result) - setCriteria({ ...result.extractedCriteria }) - setWeights(result.suggestedWeights as Record) - setWeightingKey(k => k + 1) - isManualTextRef.current = false - setInputText('') - setIsAutoGen(false) - setStep(NeedBuilderStep.IDLE) - } catch { - setError('Die KI-Analyse ist fehlgeschlagen.') - setStep(NeedBuilderStep.ERROR) - } - } - - // Resolve criteria (parse text if needed), then either search or show save preview - async function handleAction(chosenIntent: ActionIntent) { - setIntent(chosenIntent) - setError(null) - - let resolved: ParsedNeedCriteria = criteria - let resolvedResult: ParseNeedResult | null = parseResult - - if (!hasStructuredData && inputText.trim()) { - setStep(NeedBuilderStep.PARSING) - try { - const resp = await aiService.parseNeed(inputText) - resolved = resp.data.extractedCriteria - resolvedResult = resp.data - setCriteria(resolved) - setWeights(resp.data.suggestedWeights as Record) - setWeightingKey(k => k + 1) - isManualTextRef.current = false - setInputText('') - setIsAutoGen(false) - setParseResult(resp.data) - } catch { - setError('Die KI-Analyse ist fehlgeschlagen.') - setStep(NeedBuilderStep.ERROR) - return - } - } - - if (chosenIntent === 'search') { - // Save as DRAFT and navigate immediately - setStep(NeedBuilderStep.SAVING) - try { - const conf = resolvedResult - ? Object.values(resolvedResult.confidenceByField).reduce((s, v) => s + v, 0) / - Math.max(Object.values(resolvedResult.confidenceByField).length, 1) - : 0.5 - const input = buildNeedInput(resolved, weights, needTitle, conf, 'DRAFT') - const created = await needService.create(input) - await queryClient.invalidateQueries({ queryKey: ['needs'] }) - await queryClient.invalidateQueries({ queryKey: ['matches'] }) - navigate('/demand/results', { state: { fromNeedBuilder: true, activeNeedId: created.data.id } }) - } catch { - setError('Suche fehlgeschlagen.') - setStep(NeedBuilderStep.ERROR) - } - return - } - - // save-profile: show preview step - const confidenceByField: Record = resolvedResult?.confidenceByField ?? {} - if (!resolvedResult) { - if (resolved.assetType) confidenceByField.assetType = 1.0 - if (resolved.areaRange?.min) confidenceByField.areaRange = 1.0 - if (resolved.preferredLocations?.length) confidenceByField.preferredLocations = 1.0 - if (resolved.budgetRange?.maxPerSqm) confidenceByField.budgetRange = 1.0 - if (resolved.timing?.earliestMoveIn) confidenceByField.timing = 1.0 - } - setEditedCriteria({ ...resolved }) - setParseResult(resolvedResult ?? { - extractedCriteria: resolved, - confidenceByField, - missingFields: [], - assumptions: [], - suggestedWeights: weights, - followUpQuestionCandidates: [], - rawSummary: 'Manuell eingegeben', - promptVersion: 'manual', - schemaVersion: '1.0', - }) - setStep(NeedBuilderStep.READY_TO_SAVE) - } - - async function handleSaveProfile() { - if (!editedCriteria || !parseResult) return - setStep(NeedBuilderStep.SAVING) - const entries = Object.entries(parseResult.confidenceByField) - const conf = entries.length > 0 ? entries.reduce((s, [, v]) => s + v, 0) / entries.length : 0 - try { - const input = buildNeedInput(editedCriteria, weights, needTitle, conf, 'ACTIVE') - const created = await needService.create(input) - await queryClient.invalidateQueries({ queryKey: ['needs'] }) - await queryClient.invalidateQueries({ queryKey: ['matches'] }) - navigate('/demand/results', { state: { fromNeedBuilder: true, activeNeedId: created.data.id } }) - } catch { - setError('Speichern fehlgeschlagen.') - setStep(NeedBuilderStep.ERROR) - } - } - - function handleRetry() { - setStep(NeedBuilderStep.IDLE) - setError(null) - setParseResult(null) - setEditedCriteria(null) - } - - const hasStructuredData = !!( - criteria.assetType || - (criteria.areaRange?.min ?? 0) > 0 || - (criteria.preferredLocations?.length ?? 0) > 0 - ) - const canProceed = hasStructuredData || inputText.trim().length > 0 - const isProcessing = step === NeedBuilderStep.PARSING || step === NeedBuilderStep.SAVING - const isSaveStep = step === NeedBuilderStep.READY_TO_SAVE || step === NeedBuilderStep.SAVING - - const overallConfidence = parseResult - ? (() => { - const entries = Object.entries(parseResult.confidenceByField) - return entries.length > 0 ? entries.reduce((s, [, v]) => s + v, 0) / entries.length : 0 - })() - : 0 - - return ( - - {/* Header */} - - Flächensuche - - Sprechen, schreiben oder Felder ausfüllen — dann sofort suchen oder als Suchprofil speichern - - - - - - - - {/* ── IDLE: full form ── */} - {(step === NeedBuilderStep.IDLE || step === NeedBuilderStep.PARSING) && ( - - - - - - - - - - {/* Action bar */} - - - - - - - - - - Jetzt suchen liefert sofortige Ergebnisse.{' '} - Als Suchprofil speichern legt einen dauerhaften Bedarf an, der automatisch mit neuen Angeboten abgeglichen wird — auch in Zukunft. - - - )} - - {/* ── Preview + Save as Profile ── */} - {isSaveStep && parseResult && editedCriteria && ( - - - Dieses Suchprofil wird als aktiver Bedarf gespeichert und erscheint automatisch im Match Center der Verwaltung. - - - - - - - - )} - - {/* ── Error ── */} - {step === NeedBuilderStep.ERROR && ( - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/demand/Compare.tsx b/.claude/worktrees/agent-a82a3716/src/pages/demand/Compare.tsx deleted file mode 100644 index 724234a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/demand/Compare.tsx +++ /dev/null @@ -1,492 +0,0 @@ -import type { ReactNode } from 'react' -import { - Alert, - Box, - Button, - Card, - Chip, - LinearProgress, - Stack, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Typography, -} from '@mui/material' -import { Trophy, AlertTriangle, AlertOctagon, Zap, CheckCircle2, XCircle } from 'lucide-react' -import { useNavigate } from 'react-router' -import { useQuery } from '@tanstack/react-query' -import { useCompareStore } from '../../stores/compareStore' -import { aiService } from '../../services/aiService' -import { - CompareEmptyState, - CompareColumnHeader, - CompareCell, - MissingDataCell, - AICompareSummary, -} from '../../components/compare' -import type { UnifiedMatchResult } from '../../domain/unifiedResult' -import type { VerifiedPortfolioResult, ExternalMarketResult, FutureAvailabilityResult } from '../../domain/unifiedResult' - -// ── Helpers ─────────────────────────────────────────────────────────────────── - -const HARD_CRITERIA = new Set(['area', 'location', 'budget', 'timing']) -const SCORE_COLOR = (s: number) => s >= 78 ? '#1a7a4a' : s >= 52 ? '#d97706' : '#c0392b' - -function getProp(item: UnifiedMatchResult) { - return item.resultType !== 'FUTURE_AVAILABILITY' - ? (item as VerifiedPortfolioResult | ExternalMarketResult).property - : null -} - -function getSig(item: UnifiedMatchResult) { - return item.resultType === 'FUTURE_AVAILABILITY' - ? (item as FutureAvailabilityResult).signal - : null -} - -const TYPE_META: Record = { - VERIFIED_PORTFOLIO: { label: 'Verified Portfolio', color: '#1e3a5f' }, - EXTERNAL_MARKET: { label: 'Marktinserat', color: '#d97706' }, - FUTURE_AVAILABILITY: { label: 'Zukunftssignal', color: '#7c3aed' }, -} - -const RISK_LEVEL_ORDER: Record = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 } - -// ── Row label cell ──────────────────────────────────────────────────────────── - -const LABEL_SX = { - position: 'sticky' as const, - left: 0, - bgcolor: 'white', - zIndex: 1, - width: 200, - minWidth: 200, - color: '#64748b', - fontSize: 13, - fontWeight: 600, - borderRight: '1px solid #e2e8f0', - verticalAlign: 'top', - py: 1.5, -} - -const DATA_SX = { - borderLeft: '1px solid #f1f5f9', - minWidth: 220, - verticalAlign: 'top', - py: 1.5, -} - -// ── Main component ──────────────────────────────────────────────────────────── - -export default function Compare() { - const navigate = useNavigate() - const { compareItems, removeFromCompare, clearCompare } = useCompareStore() - - const { data: aiSummary, isLoading: aiLoading } = useQuery({ - queryKey: ['ai-compare', compareItems.map(i => i.matchId)], - queryFn: () => aiService.summarizeComparison(compareItems), - enabled: compareItems.length >= 2, - select: r => r.data, - staleTime: Infinity, - }) - - if (compareItems.length === 0) { - return ( - - - Vergleich - - - - ) - } - - // ── Highlight indices ────────────────────────────────────────────────────── - - const bestScoreIdx = compareItems.reduce( - (best, item, i) => item.matchScore > compareItems[best].matchScore ? i : best, 0 - ) - const worstConfIdx = compareItems.reduce( - (worst, item, i) => item.match.confidenceLevel < compareItems[worst].match.confidenceLevel ? i : worst, 0 - ) - const dqScores = compareItems.map(item => getProp(item)?.dataQuality.score ?? 1) - const worstDQIdx = dqScores.indexOf(Math.min(...dqScores)) - - const missingCriticalCounts = compareItems.map( - item => item.match.missingData?.filter(m => m.importance === 'CRITICAL').length ?? 0 - ) - const maxMissingCritical = Math.max(...missingCriticalCounts) - - // ── Shared render helpers ───────────────────────────────────────────────── - - function row(label: string, cells: ReactNode[]) { - return ( - - {label} - {cells.map((cell, i) => ( - {cell} - ))} - - ) - } - - function scoreBar(value: number, label?: string) { - const color = value >= 0.8 ? '#1a7a4a' : value >= 0.6 ? '#d97706' : '#c0392b' - return ( - - - - - {label ?? `${Math.round(value * 100)}%`} - - ) - } - - return ( - - {/* Page header */} - - - Vergleich - - - - - - {/* Mobile notice */} - - - Die Vergleichsansicht ist für Desktop optimiert. Für beste Erfahrung auf einem grösseren Bildschirm öffnen. - - - - {/* Desktop table */} - - - {/* AI Summary */} - = 2} /> - - - - - {/* Column headers */} - - - - Kriterium - - {compareItems.map(item => ( - - removeFromCompare(item.matchId)} /> - - ))} - - - - - - {/* 1. Result Type */} - {row('1. Result-Typ', compareItems.map(item => { - const m = TYPE_META[item.resultType] ?? { label: item.resultType, color: '#64748b' } - return - }))} - - {/* 2. Source / Provenance */} - {row('2. Quelle / Provenienz', compareItems.map(item => { - const prop = getProp(item) - const sig = getSig(item) - const label = prop?.sourceLabel ?? sig?.source?.type ?? null - return label - ? {label} - : - }))} - - {/* 3. Match Score */} - {row('3. Match Score', compareItems.map((item, idx) => ( - : undefined} - iconTooltip="Höchster Match Score" - > - - - {item.matchScore} - - /100 - - - )))} - - {/* 4. Confidence Score */} - {row('4. Konfidenz', compareItems.map((item, idx) => ( - : undefined} - iconTooltip="Niedrigste Konfidenz" - > - {scoreBar(item.match.confidenceLevel)} - - )))} - - {/* 5. Data Quality Score */} - {row('5. Datenqualität', compareItems.map((item, idx) => { - const prop = getProp(item) - const dq = prop?.dataQuality.score ?? null - if (dq === null) return - return ( - : undefined} - iconTooltip="Niedrigste Datenqualität" - > - {scoreBar(dq)} - - ) - }))} - - {/* 6. Asset Type */} - {row('6. Nutzungstyp', compareItems.map(item => { - const prop = getProp(item) - const label = prop?.assetType ?? null - return label - ? - : - }))} - - {/* 7. Location */} - {row('7. Standort', compareItems.map(item => { - const prop = getProp(item) - const sig = getSig(item) - const city = prop?.location?.city ?? sig?.locationHint ?? null - const district = prop?.location?.district - return city - ? {city}{district ? `, ${district}` : ''} - : - }))} - - {/* 8. Area */} - {row('8. Fläche', compareItems.map(item => { - const prop = getProp(item) - const sig = getSig(item) - const area = prop?.areaSqm ?? sig?.areaSqmEstimate ?? null - return area !== null - ? {area.toLocaleString('de-CH')} m²{sig ? ' (Schätzung)' : ''} - : - }))} - - {/* 9. Rent / Budget Fit */} - {row('9. Miete / Budget', compareItems.map(item => { - const prop = getProp(item) - if (!prop) return - return ( - - - CHF {prop.rentPricePerSqm}/m² - - {prop.totalRentMonthly && ( - - {prop.totalRentMonthly.toLocaleString('de-CH')} CHF/Monat - - )} - - ) - }))} - - {/* 10. Availability / Time Horizon */} - {row('10. Verfügbarkeit', compareItems.map(item => { - const prop = getProp(item) - const sig = getSig(item) - if (prop) return {prop.availabilityDate} - if (sig) return ( - } iconTooltip="Probabilistisches Signal — keine bestätigte Verfügbarkeit"> - ~{sig.timeHorizonMonths} Monate - - {Math.round(sig.probability * 100)}% Wahrscheinlichkeit - - - ) - return - }))} - - {/* 11. Hard Criteria Fit */} - {row('11. Hardkriterien', compareItems.map(item => { - const hardMatches = item.match.positiveFactors.filter(f => HARD_CRITERIA.has(f.criterion)) - const total = 4 - const count = hardMatches.length - const color = count >= 3 ? '#1a7a4a' : count >= 2 ? '#d97706' : '#c0392b' - return ( - - - {count}/{total} erfüllt - - - {hardMatches.map(f => ( - } - sx={{ fontSize: 10, bgcolor: '#f0fdf4', color: '#166534', '& .MuiChip-icon': { color: '#1a7a4a' } }} /> - ))} - - - ) - }))} - - {/* 12. Top Soft Factors */} - {row('12. Soft Factors', compareItems.map(item => { - const softFactors = item.match.positiveFactors - .filter(f => !HARD_CRITERIA.has(f.criterion)) - .slice(0, 3) - if (softFactors.length === 0) return - return ( - - {softFactors.map(f => ( - - ))} - - ) - }))} - - {/* 13. Main Strengths */} - {row('13. Stärken', compareItems.map(item => { - const top = item.match.positiveFactors.slice(0, 2) - if (top.length === 0) return - return ( - - {top.map((f, i) => ( - - - {f.explanation} - - ))} - - ) - }))} - - {/* 14. Main Tradeoffs */} - {row('14. Abwägungen', compareItems.map(item => { - const tradeoffs = item.match.tradeoffs?.slice(0, 2) ?? [] - if (tradeoffs.length === 0) return ( - Keine signifikanten Abwägungen - ) - return ( - - {tradeoffs.map((t, i) => ( - - - {t.criterion}: {t.concern} - - ))} - - ) - }))} - - {/* 15. Main Risks */} - {row('15. Risiken', compareItems.map(item => { - const risks = [...(item.match.risks ?? [])].sort( - (a, b) => (RISK_LEVEL_ORDER[a.level] ?? 4) - (RISK_LEVEL_ORDER[b.level] ?? 4) - ).slice(0, 2) - if (risks.length === 0) return ( - Keine identifizierten Risiken - ) - return ( - - {risks.map((r, i) => ( - - - {r.description} - - ))} - - ) - }))} - - {/* 16. Missing Data */} - {row('16. Fehlende Daten', compareItems.map((item, idx) => { - const total = item.match.missingData?.length ?? 0 - const critical = missingCriticalCounts[idx] - if (total === 0) return ( - - - Vollständig - - ) - return ( - 0 && critical === maxMissingCritical ? 'critical' : critical > 0 ? 'worst' : 'none'} - icon={critical > 0 ? : } - iconTooltip={critical > 0 ? 'Kritische Pflichtfelder fehlen' : 'Optionale Felder fehlen'} - > - {total} fehlend - {critical > 0 && ( - {critical} kritisch - )} - - ) - }))} - - {/* 17. Future Availability Context */} - {row('17. Zukunftskontext', compareItems.map(item => { - const sig = getSig(item) - if (!sig) return ( - Nicht anwendbar - ) - return ( - } iconTooltip="Probabilistisches Zukunftssignal"> - - - {Math.round(sig.probability * 100)}% Wahrscheinlichkeit - - - Sensitivität: {sig.sensitivityLevel} - - - {sig.disclaimer} - - - - ) - }))} - - {/* 18. Recommended Next Action */} - {row('18. Nächste Aktion', compareItems.map(item => { - const action = item.match.nextBestActions?.[0] - if (!action) return - return ( - - {action.label} - {action.description && ( - {action.description} - )} - - ) - }))} - - -
-
- - {/* Add more prompt */} - {compareItems.length < 4 && ( - - - - Weiteres Ergebnis hinzufügen - - Bis zu {4 - compareItems.length} weitere{4 - compareItems.length === 1 ? 's' : ''} Ergebnis{4 - compareItems.length === 1 ? '' : 'se'} möglich - - - - - - )} -
-
- ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/demand/MatchDetail.tsx b/.claude/worktrees/agent-a82a3716/src/pages/demand/MatchDetail.tsx deleted file mode 100644 index adc928b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/demand/MatchDetail.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { Box, CircularProgress, Paper, Typography } from '@mui/material' -import { useNavigate, useParams } from 'react-router' -import { useQuery } from '@tanstack/react-query' -import { useMatchDetail } from '../../hooks/useMatches' -import { propertyService } from '../../services/propertyService' -import { needService } from '../../services/needService' -import { futureSignalService } from '../../services/futureSignalService' -import { useCompareStore } from '../../stores/compareStore' -import { useShortlistStore } from '../../stores/shortlistStore' -import { AddToShortlistDialog } from '../../components/shortlist' -import { MatchReasonList } from '../../components/match-card/MatchReasonList' -import { - LocationIntelligencePanel, - MatchDetailHeader, - ExecutiveSummaryPanel, - PropertyOverviewPanel, - NeedAlignmentPanel, - ScoreBreakdownPanel, - TradeoffPanel, - RiskPanel, - MissingInformationPanel, - SourceProvenancePanel, - FutureAvailabilityContextPanel, - NextActionsPanel, -} from '../../components/match-detail' -import type { MatchCardReason } from '../../components/match-card/MatchCardViewModel' - -const HARD_CRITERIA = new Set(['area', 'location', 'budget', 'timing']) - -function buildReasons(match: NonNullable['data']>): MatchCardReason[] { - return match.positiveFactors.slice(0, 3).map(f => ({ - type: (HARD_CRITERIA.has(f.criterion) ? 'HARD_FACT' : 'SOFT_FACTOR') as MatchCardReason['type'], - label: f.criterion.charAt(0).toUpperCase() + f.criterion.slice(1), - explanation: f.explanation, - score: f.score, - })) -} - -export default function MatchDetail() { - const { matchId } = useParams<{ matchId: string }>() - const navigate = useNavigate() - const { addToCompare } = useCompareStore() - const { openAddDialog } = useShortlistStore() - - const { data: match, isLoading } = useMatchDetail(matchId ?? '') - - const isFuture = match?.resultType === 'FUTURE_AVAILABILITY' - - - const { data: property = null } = useQuery({ - queryKey: ['property', match?.propertyId], - queryFn: () => propertyService.getById(match!.propertyId), - enabled: !!match && !isFuture, - select: r => r.data ?? null, - }) - - const { data: need = null } = useQuery({ - queryKey: ['need', match?.needId], - queryFn: () => needService.getById(match!.needId), - enabled: !!match?.needId, - select: r => r.data ?? null, - }) - - const { data: signal = null } = useQuery({ - queryKey: ['signal', match?.resultId], - queryFn: () => futureSignalService.getById(match!.resultId!), - enabled: !!match && isFuture && !!match.resultId, - select: r => r.data ?? null, - }) - - if (isLoading) { - return ( - - - - ) - } - - if (!match) { - return ( - - - Match nicht gefunden - - Das gesuchte Match existiert nicht oder wurde entfernt. - - - - ) - } - - const reasons = buildReasons(match) - - const handleCompare = () => { - if (match && !isFuture && property) { - addToCompare({ - resultType: property.resultType === 'EXTERNAL_MARKET' ? 'EXTERNAL_MARKET' : 'VERIFIED_PORTFOLIO', - matchId: match.id, - needId: match.needId, - matchScore: match.matchScore, - match, - property, - }) - } else if (match && isFuture && signal) { - addToCompare({ - resultType: 'FUTURE_AVAILABILITY', - matchId: match.id, - needId: match.needId, - matchScore: match.matchScore, - match, - signal, - }) - } - navigate('/demand/compare') - } - - const handleBack = () => navigate(-1) - - const handleShortlist = () => { - const title = property?.title ?? signal?.companyName ?? signal?.locationHint ?? match.id - openAddDialog({ - resultId: match.id, - resultType: match.resultType ?? 'VERIFIED_PORTFOLIO', - title, - matchScore: match.matchScore, - confidenceScore: match.confidenceLevel, - sourceLabel: property?.sourceLabel ?? match.resultType ?? 'VERIFIED_PORTFOLIO', - addedBy: 'admin@ideal-sharing.ch', - propertyId: property?.id, - }) - } - - return ( - - - - - - {/* Main column */} - - - - - - {/* Why It Matches — structured from scoreFactors */} - {reasons.length > 0 && ( - - Warum dieses Match - - {match.negativeFactors.length > 0 && ( - - - Schwächere Faktoren - - - {match.negativeFactors.slice(0, 3).map((f, i) => ( - - · {f.criterion}: {f.explanation} - - ))} - - - )} - - )} - - - - - - - {isFuture && } - - - {/* Sidebar — sticky */} - - - {}} - onReject={() => {}} - /> - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/demand/Results.tsx b/.claude/worktrees/agent-a82a3716/src/pages/demand/Results.tsx deleted file mode 100644 index 35a6a9a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/demand/Results.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useState } from 'react' -import { Box, Button, Card, Typography } from '@mui/material' -import { useNavigate, useLocation } from 'react-router' -import { useQuery, useQueryClient } from '@tanstack/react-query' -import { useUnifiedResults } from '../../hooks/useUnifiedResults' -import { needService } from '../../services/needService' -import { DecisionContextPanel } from '../../components/ui' -import { - FeedEmptyState, - FeedSkeleton, - ResultFeedHeader, - ResultFilterBar, - UnifiedResultFeed, -} from '../../components/results' -import { AddToShortlistDialog } from '../../components/shortlist' -import type { ResultType } from '../../domain/enums' -import type { UnifiedMatchResult } from '../../domain/unifiedResult' - -type FilterSource = ResultType | 'ALL' -type SortBy = 'score' | 'rent' | 'area' - -function sortResults(results: UnifiedMatchResult[], sortBy: SortBy): UnifiedMatchResult[] { - return [...results].sort((a, b) => { - if (sortBy === 'score') return b.matchScore - a.matchScore - const propA = a.resultType !== 'FUTURE_AVAILABILITY' ? (a as { property: { rentPricePerSqm: number; areaSqm: number } }).property : null - const propB = b.resultType !== 'FUTURE_AVAILABILITY' ? (b as { property: { rentPricePerSqm: number; areaSqm: number } }).property : null - if (sortBy === 'rent') return (propA?.rentPricePerSqm ?? 0) - (propB?.rentPricePerSqm ?? 0) - if (sortBy === 'area') return (propB?.areaSqm ?? 0) - (propA?.areaSqm ?? 0) - return 0 - }) -} - -export default function Results() { - const navigate = useNavigate() - const location = useLocation() - const queryClient = useQueryClient() - const [filterSource, setFilterSource] = useState('ALL') - const [sortBy, setSortBy] = useState('score') - - // When coming from NeedBuilder, invalidate so the freshly created need is included - const activeNeedIdFromNav = (location.state as { activeNeedId?: string } | null)?.activeNeedId - - const { data: needResp } = useQuery({ - queryKey: ['needs'], - queryFn: () => needService.getAll(), - // Refetch on mount when navigating from NeedBuilder to pick up the new need - refetchOnMount: activeNeedIdFromNav ? 'always' : true, - gcTime: 0, - }) - - // Prefer the ID passed from NeedBuilder; fall back to most-recently-created - const activeNeed = needResp?.data - ? activeNeedIdFromNav - ? (needResp.data.find(n => n.id === activeNeedIdFromNav) ?? needResp.data[0]) - : [...needResp.data].sort((a, b) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - )[0] - : undefined - - // Ensure query cache is invalidated when a new need was just created - if (activeNeedIdFromNav) { - queryClient.invalidateQueries({ queryKey: ['needs'] }) - } - - const { data: results = [], isLoading } = useUnifiedResults(activeNeed?.id) - - const filtered = - filterSource === 'ALL' ? results : results.filter(r => r.resultType === filterSource) - - const sorted = sortResults(filtered, sortBy) - - const verifiedCount = results.filter(r => r.resultType === 'VERIFIED_PORTFOLIO').length - const externalCount = results.filter(r => r.resultType === 'EXTERNAL_MARKET').length - const futureCount = results.filter(r => r.resultType === 'FUTURE_AVAILABILITY').length - const strongCount = results.filter(r => r.matchScore >= 80).length - const missingDataCount = results.filter(r => - 'match' in r && Array.isArray((r as { match?: { missingData?: unknown[] } }).match?.missingData) && - ((r as { match?: { missingData?: unknown[] } }).match?.missingData?.length ?? 0) > 0 - ).length - - return ( - - - - - {!isLoading && results.length > 0 && ( - 0 ? [{ label: 'starke Treffer (≥80)', value: strongCount, severity: 'positive' as const }] : []), - ...(verifiedCount > 0 ? [{ label: 'verifiziertes Portfolio', value: verifiedCount, severity: 'neutral' as const }] : []), - ...(externalCount > 0 ? [{ label: 'externer Markt', value: externalCount, severity: 'neutral' as const }] : []), - ...(futureCount > 0 ? [{ label: 'Zukunftssignale', value: futureCount, severity: 'warning' as const }] : []), - ...(missingDataCount > 0 ? [{ label: 'mit Datenlücken', value: missingDataCount, severity: 'warning' as const }] : []), - ]} - risks={[ - ...(futureCount > 0 ? [`${futureCount} Zukunftssignal${futureCount > 1 ? 'e' : ''} sind probabilistisch — keine bestätigte Verfügbarkeit`] : []), - ...(missingDataCount > 0 ? [`${missingDataCount} Treffer mit fehlenden Daten — Einschätzung eingeschränkt`] : []), - ]} - actions={[ - { label: 'Vergleich öffnen', onClick: () => navigate('/demand/compare') }, - { label: 'Suche anpassen', onClick: () => navigate('/demand/ai-search') }, - ]} - /> - )} - - - {activeNeed && ( - - - - - Aktive Suche: {activeNeed.companyName} - - - - Typ: {activeNeed.assetType} - - - Fläche: {activeNeed.requiredArea.min}–{activeNeed.requiredArea.max} m² - - - Standort: {activeNeed.preferredLocations.join(', ')} - - {activeNeed.budgetRange && ( - - Budget: max. CHF {activeNeed.budgetRange.maxPerSqm}/m² - - )} - {activeNeed.timing && ( - - Bezug ab: {new Date(activeNeed.timing.earliestMoveIn).toLocaleDateString('de-CH', { month: 'long', year: 'numeric' })} - - )} - {activeNeed.mustCriteriaText && activeNeed.mustCriteriaText.length > 0 && ( - - Must-haves: {activeNeed.mustCriteriaText.slice(0, 3).join(' · ')} - - )} - - - - - - )} - - - - {isLoading ? ( - - ) : sorted.length === 0 ? ( - - ) : ( - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/demand/Shortlists.tsx b/.claude/worktrees/agent-a82a3716/src/pages/demand/Shortlists.tsx deleted file mode 100644 index 2690a3a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/demand/Shortlists.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Box, Paper, Typography } from '@mui/material' -import { useShortlists } from '../../hooks/useShortlists' -import { useShortlistStore } from '../../stores/shortlistStore' -import { - ShortlistList, - ShortlistDetail, - DecisionBriefDraftPanel, - AddToShortlistDialog, -} from '../../components/shortlist' - -const PANEL_HEADER_SX = { - px: 2, - py: 1.5, - borderBottom: '1px solid #e2e8f0', - bgcolor: 'white', - position: 'sticky' as const, - top: 0, - zIndex: 1, - flexShrink: 0, -} - -export default function Shortlists() { - const { data: shortlists = [], isLoading } = useShortlists() - const { selectedShortlistId } = useShortlistStore() - - const selectedShortlist = shortlists.find(s => s.id === selectedShortlistId) ?? null - - return ( - - - {/* Left: shortlist list */} - - - Shortlists - {shortlists.length} gespeichert - - - - - {/* Center: detail */} - - {selectedShortlist ? ( - - ) : ( - - - Shortlist aus der Liste wählen - - - )} - - - {/* Right: decision brief — only when a shortlist is selected */} - {selectedShortlist && ( - - - - )} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/ops/AIMonitoring.tsx b/.claude/worktrees/agent-a82a3716/src/pages/ops/AIMonitoring.tsx deleted file mode 100644 index 96ba529..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/ops/AIMonitoring.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { useState } from 'react' -import { Box, Chip, MenuItem, Select, Typography } from '@mui/material' -import { Bot } from 'lucide-react' -import { - AIMonitoringMetrics, - AIOutputTable, - AIOutputDetailPanel, - AIMonitoringEmptyState, -} from '../../components/ai-monitoring' -import { useAIOutputs, useUpdateAIOutputReviewStatus } from '../../hooks/useAIMonitoring' -import { useToastStore } from '../../stores/toastStore' -import type { AIOutput, AIOutputType } from '../../domain/aiOutput' -import type { ReviewStatus } from '../../domain/enums' - -interface Filters { - type: AIOutputType | '' - reviewStatus: ReviewStatus | '' - hasError: boolean | null -} - -const TYPE_LABELS: Record = { - NEED_PARSE: 'Bedarf-Parsing', - FOLLOW_UP_QUESTIONS: 'Rückfragen', - MATCH_EXPLANATION: 'Match-Begründung', - COMPARE_SUMMARY: 'Vergleich', - DECISION_BRIEF: 'Entscheidungs-Brief', - DATA_QUALITY_SUMMARY: 'Datenqualität', -} - -const AI_OUTPUT_TYPES: AIOutputType[] = [ - 'NEED_PARSE', - 'FOLLOW_UP_QUESTIONS', - 'MATCH_EXPLANATION', - 'COMPARE_SUMMARY', - 'DECISION_BRIEF', - 'DATA_QUALITY_SUMMARY', -] - -const REVIEW_STATUSES: ReviewStatus[] = ['UNREVIEWED', 'IN_REVIEW', 'APPROVED', 'REJECTED', 'FLAGGED'] -const STATUS_LABELS: Record = { - UNREVIEWED: 'Ungeprüft', - IN_REVIEW: 'In Prüfung', - APPROVED: 'Genehmigt', - REJECTED: 'Abgelehnt', - FLAGGED: 'Markiert', -} - -function applyFilters(outputs: AIOutput[], filters: Filters): AIOutput[] { - return outputs.filter(o => { - if (filters.type && o.type !== filters.type) return false - if (filters.reviewStatus && o.reviewStatus !== filters.reviewStatus) return false - if (filters.hasError === true && !o.error) return false - if (filters.hasError === false && !!o.error) return false - return true - }) -} - -export default function AIMonitoring() { - const [filters, setFilters] = useState({ type: '', reviewStatus: '', hasError: null }) - const [selectedOutput, setSelectedOutput] = useState(null) - - const showToast = useToastStore((s) => s.showToast) - const { data: allOutputs = [], isLoading } = useAIOutputs() - const updateStatus = useUpdateAIOutputReviewStatus() - - const filtered = applyFilters(allOutputs, filters) - - const failedCount = allOutputs.filter(o => !!o.error).length - const pendingCount = allOutputs.filter(o => o.reviewStatus === 'UNREVIEWED' || o.reviewStatus === 'FLAGGED').length - - const STATUS_TOAST: Record = { - UNREVIEWED: 'Status zurückgesetzt.', - IN_REVIEW: 'Output zur Prüfung markiert.', - APPROVED: 'Output genehmigt.', - REJECTED: 'Output abgelehnt.', - FLAGGED: 'Output markiert.', - } - - const handleUpdateStatus = (status: ReviewStatus) => { - if (!selectedOutput) return - updateStatus.mutate( - { id: selectedOutput.id, status }, - { - onSuccess: (res) => { - setSelectedOutput(res.data) - showToast(STATUS_TOAST[status] ?? 'Status aktualisiert.') - }, - onError: () => showToast('Statusänderung fehlgeschlagen.', 'error'), - } - ) - } - - const activeFilterCount = [filters.type, filters.reviewStatus, filters.hasError !== null].filter(Boolean).length - - return ( - - {/* Header */} - - - - - AI Monitoring - - - {failedCount > 0 && ( - - )} - {pendingCount > 0 && ( - - )} - - - - Transparenz und Governance für KI-generierte Outputs - - - - {/* Metrics strip */} - {!isLoading && } - - {/* Filter bar */} - - - - - - setFilters(f => ({ ...f, hasError: f.hasError === true ? null : true }))} - sx={{ - cursor: 'pointer', - bgcolor: filters.hasError === true ? '#fee2e2' : '#f1f5f9', - color: filters.hasError === true ? '#991b1b' : '#64748b', - fontWeight: filters.hasError === true ? 700 : 400, - fontSize: '0.75rem', - }} - /> - - {activeFilterCount > 0 && ( - setFilters({ type: '', reviewStatus: '', hasError: null })} - sx={{ cursor: 'pointer', fontSize: '0.75rem', color: '#64748b' }} - /> - )} - - - {activeFilterCount > 0 ? `${filtered.length} / ${allOutputs.length}` : `${allOutputs.length} Outputs`} - - - - {/* Body */} - - {/* Left: table */} - - {isLoading ? ( - - Laden… - - ) : ( - - )} - - - {/* Right: detail panel */} - - {selectedOutput ? ( - setSelectedOutput(null)} - onUpdateStatus={handleUpdateStatus} - isSubmitting={updateStatus.isPending} - /> - ) : null} - - - {/* Empty selection hint when no panel open */} - {!selectedOutput && filtered.length > 0 && ( - - - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/ops/ActivityTimeline.tsx b/.claude/worktrees/agent-a82a3716/src/pages/ops/ActivityTimeline.tsx deleted file mode 100644 index 1cebbaa..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/ops/ActivityTimeline.tsx +++ /dev/null @@ -1,362 +0,0 @@ -import { useState } from 'react' -import { Box, Chip, CircularProgress, Stack, Tooltip, Typography } from '@mui/material' -import { - Activity, - Bot, - Bookmark, - BookmarkCheck, - Building2, - AlertTriangle, - CheckCircle, - ClipboardList, - Edit, - FileText, - GitMerge, - Radar, - Search, - ServerCog, - TrendingUp, - User, - XCircle, -} from 'lucide-react' -import { useQuery } from '@tanstack/react-query' -import { governanceService, type ActivityCategory, type ActivityEvent, type ActivityEventType } from '../../services/governanceService' -import { EmptyState } from '../../components/ui' - -// ── Labels & colours ────────────────────────────────────────────────────────── - -const EVENT_LABELS: Record = { - PROPERTY_CREATED: 'Objekt erstellt', - PROPERTY_UPDATED: 'Objekt aktualisiert', - MATCH_APPROVED: 'Match genehmigt', - MATCH_REJECTED: 'Match abgelehnt', - SIGNAL_VERIFIED: 'Signal verifiziert', - NEED_CREATED: 'Bedarf erstellt', - REVIEW_REQUESTED: 'Überprüfung angefordert', - AI_PARSE_COMPLETED: 'KI-Analyse abgeschlossen', - AI_OUTPUT_REVIEWED: 'KI-Output geprüft', - MATCH_GENERATED: 'Matches generiert', - FUTURE_SIGNAL_DETECTED: 'Zukunftssignal erkannt', - FUTURE_SIGNAL_CONVERTED: 'Signal konvertiert', - SHORTLIST_CREATED: 'Shortlist erstellt', - SHORTLIST_FINALIZED: 'Shortlist finalisiert', - DECISION_BRIEF_CREATED: 'Entscheidungsbriefing erstellt', - SOURCE_CRAWLED: 'Quelle gecrawlt', - DATA_QUALITY_FLAGGED: 'Datenqualität markiert', - REVIEW_COMPLETED: 'Prüfung abgeschlossen', -} - -const EVENT_COLORS: Record = { - PROPERTY_CREATED: '#1e3a5f', - PROPERTY_UPDATED: '#1e3a5f', - MATCH_APPROVED: '#1a7a4a', - MATCH_REJECTED: '#c0392b', - SIGNAL_VERIFIED: '#7c3aed', - NEED_CREATED: '#0891b2', - REVIEW_REQUESTED: '#d97706', - AI_PARSE_COMPLETED: '#0891b2', - AI_OUTPUT_REVIEWED: '#1a7a4a', - MATCH_GENERATED: '#0891b2', - FUTURE_SIGNAL_DETECTED: '#7c3aed', - FUTURE_SIGNAL_CONVERTED: '#7c3aed', - SHORTLIST_CREATED: '#0f766e', - SHORTLIST_FINALIZED: '#0f766e', - DECISION_BRIEF_CREATED: '#0f766e', - SOURCE_CRAWLED: '#6366f1', - DATA_QUALITY_FLAGGED: '#ea580c', - REVIEW_COMPLETED: '#1a7a4a', -} - -const CATEGORY_META: Record = { - SUCHE: { label: 'Suche', color: '#0891b2' }, - MATCHING: { label: 'Matching', color: '#1a7a4a' }, - INTELLIGENCE: { label: 'Intelligence', color: '#7c3aed' }, - REVIEW: { label: 'Review', color: '#d97706' }, - GOVERNANCE: { label: 'Governance', color: '#1e3a5f' }, -} - -const ALL_CATEGORIES: ActivityCategory[] = ['SUCHE', 'MATCHING', 'INTELLIGENCE', 'REVIEW', 'GOVERNANCE'] - -function getEventIcon(type: ActivityEventType) { - const s = 13 - 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 - case 'AI_PARSE_COMPLETED': - case 'AI_OUTPUT_REVIEWED': - case 'MATCH_GENERATED': - case 'DECISION_BRIEF_CREATED': return - case 'FUTURE_SIGNAL_DETECTED': - case 'FUTURE_SIGNAL_CONVERTED': return - case 'SHORTLIST_CREATED': return - case 'SHORTLIST_FINALIZED': return - case 'SOURCE_CRAWLED': return - case 'DATA_QUALITY_FLAGGED': return - case 'REVIEW_COMPLETED': return - default: return - } -} - -function formatDate(iso: string): string { - return new Date(iso).toLocaleDateString('de-CH', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' }) -} - -function formatTime(iso: string): string { - return new Date(iso).toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' }) -} - -function dateKey(iso: string): string { - return iso.slice(0, 10) -} - -function isToday(iso: string): boolean { - return dateKey(iso) === new Date().toISOString().slice(0, 10) -} - -function groupByDate(events: ActivityEvent[]): { key: string; label: string; events: ActivityEvent[] }[] { - const map = new Map() - for (const e of events) { - const k = dateKey(e.createdAt) - if (!map.has(k)) map.set(k, []) - map.get(k)!.push(e) - } - return [...map.entries()] - .sort(([a], [b]) => b.localeCompare(a)) - .map(([key, evts]) => ({ - key, - label: isToday(evts[0].createdAt) ? 'Heute' : formatDate(evts[0].createdAt), - events: evts, - })) -} - -// ── EventRow ────────────────────────────────────────────────────────────────── - -function EventRow({ event, isLast }: { event: ActivityEvent; isLast: boolean }) { - const color = EVENT_COLORS[event.type] ?? '#64748b' - const catMeta = CATEGORY_META[event.category] - - return ( - - {/* Vertical connector */} - {!isLast && ( - - )} - - {/* Icon dot */} - - {getEventIcon(event.type)} - - - {/* Content */} - - - - - - {EVENT_LABELS[event.type]} - - - {event.isAiAction && ( - - - - )} - - {event.notes && ( - - {event.notes} - - )} - - {event.isAiAction - ? - : - } - - {event.isAiAction ? 'KI-System' : event.performedBy} - - - - - {formatTime(event.createdAt)} - - - - - ) -} - -// ── Page ────────────────────────────────────────────────────────────────────── - -export default function ActivityTimeline() { - const [filterCategory, setFilterCategory] = useState('ALL') - - const { data: resp, isLoading, error } = useQuery({ - queryKey: ['activityTimeline', 'org-wincasa'], - queryFn: () => governanceService.getActivityLog('org-wincasa'), - staleTime: 30_000, - }) - - const allEvents = resp?.data ?? [] - - const filtered = filterCategory === 'ALL' - ? allEvents - : allEvents.filter(e => e.category === filterCategory) - - const grouped = groupByDate(filtered) - - const aiCount = allEvents.filter(e => e.isAiAction).length - const humanCount = allEvents.length - aiCount - const uniqueActors = new Set(allEvents.map(e => e.performedBy)).size - const todayCount = allEvents.filter(e => isToday(e.createdAt)).length - - if (isLoading) { - return ( - - - - ) - } - - if (error) { - return ( - - Aktivitätsverlauf konnte nicht geladen werden. - - ) - } - - return ( - - {/* Header */} - - - - - Aktivitäts-Timeline - - {/* KPI chips */} - - - {todayCount > 0 && ( - - )} - - - - - - - End-to-End Aktivitätsverlauf — KI-Aktionen und Menschliche Entscheidungen im Überblick - - - - {/* Filter bar */} - - - setFilterCategory('ALL')} - sx={{ - bgcolor: filterCategory === 'ALL' ? '#1e3a5f' : 'transparent', - color: filterCategory === 'ALL' ? 'white' : '#64748b', - border: `1px solid ${filterCategory === 'ALL' ? '#1e3a5f' : '#e2e8f0'}`, - fontWeight: filterCategory === 'ALL' ? 700 : 400, - fontSize: '0.75rem', - }} - /> - {ALL_CATEGORIES.map(cat => { - const meta = CATEGORY_META[cat] - const active = filterCategory === cat - return ( - setFilterCategory(cat)} - sx={{ - bgcolor: active ? meta.color : 'transparent', - color: active ? 'white' : meta.color, - border: `1px solid ${active ? meta.color : meta.color + '40'}`, - fontWeight: active ? 700 : 400, - fontSize: '0.75rem', - }} - /> - ) - })} - {filterCategory !== 'ALL' && ( - - {filtered.length} von {allEvents.length} - - )} - - - - {/* Timeline body */} - - {grouped.length === 0 ? ( - - ) : ( - - {grouped.map(({ key, label, events: dayEvents }) => ( - - {/* Day header */} - - - {label} - - - - {dayEvents.length} Ereignisse - - - - {/* Events */} - - {dayEvents.map((evt, idx) => ( - - ))} - - - ))} - - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/ops/Governance.tsx b/.claude/worktrees/agent-a82a3716/src/pages/ops/Governance.tsx deleted file mode 100644 index 948a6c2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/ops/Governance.tsx +++ /dev/null @@ -1,361 +0,0 @@ -import { useState } from 'react' -import { - Box, - Button, - Card, - Chip, - Typography, - Stack, - CircularProgress, -} from '@mui/material' -import { - Building2, - Edit, - CheckCircle, - XCircle, - TrendingUp, - Search, - ClipboardList, - Bot, - Radar, - ServerCog, - Bookmark, - BookmarkCheck, - AlertTriangle, - GitMerge, -} 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' - case 'AI_PARSE_COMPLETED': return 'KI-Analyse abgeschlossen' - case 'AI_OUTPUT_REVIEWED': return 'KI-Output geprüft' - case 'MATCH_GENERATED': return 'Matches generiert' - case 'FUTURE_SIGNAL_DETECTED': return 'Zukunftssignal erkannt' - case 'FUTURE_SIGNAL_CONVERTED': return 'Signal konvertiert' - case 'SHORTLIST_CREATED': return 'Shortlist erstellt' - case 'SHORTLIST_FINALIZED': return 'Shortlist finalisiert' - case 'DECISION_BRIEF_CREATED': return 'Entscheidungsbriefing erstellt' - case 'SOURCE_CRAWLED': return 'Quelle gecrawlt' - case 'DATA_QUALITY_FLAGGED': return 'Datenqualität markiert' - case 'REVIEW_COMPLETED': return 'Prüfung abgeschlossen' - } -} - -function getEventDescription(event: ActivityEvent): string { - const actor = event.isAiAction ? 'KI-System' : event.performedBy - const action = getEventLabel(event.type) - return `${actor} — ${action}` -} - -function getEventColor(type: ActivityEventType): string { - switch (type) { - case 'PROPERTY_CREATED': - case 'PROPERTY_UPDATED': return '#1e3a5f' - case 'MATCH_APPROVED': - case 'REVIEW_COMPLETED': return '#1a7a4a' - case 'MATCH_REJECTED': return '#c0392b' - case 'SIGNAL_VERIFIED': - case 'FUTURE_SIGNAL_DETECTED': - case 'FUTURE_SIGNAL_CONVERTED': return '#7c3aed' - case 'NEED_CREATED': - case 'AI_PARSE_COMPLETED': - case 'MATCH_GENERATED': return '#0891b2' - case 'REVIEW_REQUESTED': return '#d97706' - case 'AI_OUTPUT_REVIEWED': return '#1a7a4a' - case 'SHORTLIST_CREATED': - case 'SHORTLIST_FINALIZED': - case 'DECISION_BRIEF_CREATED': return '#0f766e' - case 'SOURCE_CRAWLED': return '#6366f1' - case 'DATA_QUALITY_FLAGGED': return '#ea580c' - } -} - -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 - case 'AI_PARSE_COMPLETED': - case 'AI_OUTPUT_REVIEWED': - case 'MATCH_GENERATED': - case 'DECISION_BRIEF_CREATED': return - case 'FUTURE_SIGNAL_DETECTED': - case 'FUTURE_SIGNAL_CONVERTED': return - case 'SHORTLIST_CREATED': return - case 'SHORTLIST_FINALIZED': return - case 'SOURCE_CRAWLED': return - case 'DATA_QUALITY_FLAGGED': return - case 'REVIEW_COMPLETED': return - } -} - -const ALL_EVENT_TYPES: ActivityEventType[] = [ - 'PROPERTY_CREATED', - 'PROPERTY_UPDATED', - 'MATCH_APPROVED', - 'MATCH_REJECTED', - 'SIGNAL_VERIFIED', - 'NEED_CREATED', - 'REVIEW_REQUESTED', - 'AI_PARSE_COMPLETED', - 'AI_OUTPUT_REVIEWED', - 'MATCH_GENERATED', - 'FUTURE_SIGNAL_DETECTED', - 'FUTURE_SIGNAL_CONVERTED', - 'SHORTLIST_CREATED', - 'SHORTLIST_FINALIZED', - 'DECISION_BRIEF_CREATED', - 'SOURCE_CRAWLED', - 'DATA_QUALITY_FLAGGED', - 'REVIEW_COMPLETED', -] - -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/.claude/worktrees/agent-a82a3716/src/pages/ops/MarketIntelligence.tsx b/.claude/worktrees/agent-a82a3716/src/pages/ops/MarketIntelligence.tsx deleted file mode 100644 index a81c572..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/ops/MarketIntelligence.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useState } from 'react' -import { Box } from '@mui/material' -import { PageHeader } from '../../components/layout' -import { SignalInbox, MarketSignalDetailPanel } from '../../components/ops' -import { useMarketSignals } from '../../hooks/useMarketSignals' -import type { MarketSignalFilters } from '../../domain/marketSignal' - -export default function MarketIntelligence() { - const [selectedId, setSelectedId] = useState(null) - const [filters, setFilters] = useState({}) - const { data: signals = [], isLoading } = useMarketSignals(filters) - const selectedSignal = signals.find((s) => s.id === selectedId) ?? null - - return ( - - - - - - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/ops/ReviewQueue.tsx b/.claude/worktrees/agent-a82a3716/src/pages/ops/ReviewQueue.tsx deleted file mode 100644 index 8878380..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/ops/ReviewQueue.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { useState } from 'react' -import { Box, Chip, Typography } from '@mui/material' -import { ShieldCheck } from 'lucide-react' -import { - ReviewFilterBar, - ReviewTaskCard, - ReviewDetailPanel, - ReviewEmptyState, -} from '../../components/review' -import { useReviewQueue, useUpdateReviewStatus, useAddReviewNote } from '../../hooks/useReviewQueue' -import { useSessionStore } from '../../stores/sessionStore' -import { useToastStore } from '../../stores/toastStore' -import type { ReviewTask, ReviewTaskStatus } from '../../domain/review' -import type { ReviewFilters } from '../../provider/IReviewProvider' - -function filterTasks(tasks: ReviewTask[], filters: ReviewFilters): ReviewTask[] { - return tasks.filter(t => { - if (filters.status && t.status !== filters.status) return false - if (filters.priority && t.priority !== filters.priority) return false - if (filters.entityType && t.entityType !== filters.entityType) return false - if (filters.assignedTo && t.assignedTo !== filters.assignedTo) return false - return true - }) -} - -export default function ReviewQueue() { - const { currentUser } = useSessionStore() - const showToast = useToastStore((s) => s.showToast) - const userRole = currentUser?.role ?? 'REVIEWER' - - const [filters, setFilters] = useState({}) - const [selectedTask, setSelectedTask] = useState(null) - - const { data: allTasks = [], isLoading } = useReviewQueue() - const updateStatus = useUpdateReviewStatus() - const addNote = useAddReviewNote() - - const filtered = filterTasks(allTasks, filters) - - const pendingCount = allTasks.filter(t => t.status === 'PENDING').length - const inReviewCount = allTasks.filter(t => t.status === 'IN_REVIEW').length - const escalatedCount = allTasks.filter(t => t.status === 'ESCALATED').length - const criticalCount = allTasks.filter(t => t.priority === 'CRITICAL' && (t.status === 'PENDING' || t.status === 'IN_REVIEW')).length - - const STATUS_TOAST: Record = { - PENDING: 'Status auf "Ausstehend" gesetzt.', - IN_REVIEW: 'Aufgabe zur Prüfung übernommen.', - APPROVED: 'Aufgabe genehmigt.', - REJECTED: 'Aufgabe abgelehnt.', - ESCALATED: 'Aufgabe eskaliert.', - NEEDS_MORE_DATA: 'Weitere Daten angefordert.', - } - - const handleAction = (status: ReviewTaskStatus) => { - if (!selectedTask) return - updateStatus.mutate( - { id: selectedTask.id, status }, - { - onSuccess: (res) => { - setSelectedTask(res.data) - showToast(STATUS_TOAST[status] ?? 'Status aktualisiert.') - }, - onError: () => showToast('Statusänderung fehlgeschlagen.', 'error'), - } - ) - } - - const handleAddNote = (content: string) => { - if (!selectedTask) return - addNote.mutate( - { id: selectedTask.id, content }, - { - onSuccess: (res) => { - setSelectedTask(res.data) - showToast('Notiz hinzugefügt.') - }, - onError: () => showToast('Notiz konnte nicht gespeichert werden.', 'error'), - } - ) - } - - const handleSelect = (task: ReviewTask) => { - setSelectedTask(task) - } - - const isSubmitting = updateStatus.isPending || addNote.isPending - - if (!currentUser || !currentUser.allowedWorkspaces.includes('OPERATIONS')) { - return ( - - - - ) - } - - return ( - - {/* Header */} - - - - - Review Queue - - - {pendingCount > 0 && ( - - )} - {inReviewCount > 0 && ( - - )} - {escalatedCount > 0 && ( - - )} - {criticalCount > 0 && ( - - )} - - - - Human-in-the-loop Governance für KI-Outputs, Matches und Datenfehler - - - - {/* Filter bar */} - - - {/* Body */} - - {/* Left: task list */} - - {isLoading ? ( - - Laden… - - ) : filtered.length === 0 ? ( - - ) : ( - - {filtered.map(task => ( - - ))} - - )} - - - {/* Right: detail panel */} - - {selectedTask ? ( - setSelectedTask(null)} - onAction={handleAction} - onAddNote={handleAddNote} - isSubmitting={isSubmitting} - /> - ) : ( - - )} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/ops/SignalPipeline.tsx b/.claude/worktrees/agent-a82a3716/src/pages/ops/SignalPipeline.tsx deleted file mode 100644 index f524a3e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/ops/SignalPipeline.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useState } from 'react' -import { Box } from '@mui/material' -import { PageHeader } from '../../components/layout' -import { SignalInbox, SignalPipelineView, MarketSignalEmptyState } from '../../components/ops' -import { useMarketSignals } from '../../hooks/useMarketSignals' -import type { MarketSignalFilters } from '../../domain/marketSignal' - -export default function SignalPipeline() { - const [selectedId, setSelectedId] = useState(null) - const [filters, setFilters] = useState({}) - const { data: signals = [], isLoading } = useMarketSignals(filters) - const selectedSignal = signals.find((s) => s.id === selectedId) ?? null - - return ( - - - - - - - - {selectedSignal - ? - : - } - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/ops/SourceMonitoring.tsx b/.claude/worktrees/agent-a82a3716/src/pages/ops/SourceMonitoring.tsx deleted file mode 100644 index ed0f42e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/ops/SourceMonitoring.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useState } from 'react' -import { Box } from '@mui/material' -import { PageHeader } from '../../components/layout' -import { SourceList, SourceDetailPanel } from '../../components/ops' -import { useDataSources, useDataSource } from '../../hooks/useDataSources' -import type { SourceFilters } from '../../domain/dataSource' - -export default function SourceMonitoring() { - const [selectedId, setSelectedId] = useState(null) - const [filters, setFilters] = useState({}) - - const { data: sources = [], isLoading } = useDataSources(filters) - const { data: selectedSource = null } = useDataSource(selectedId) - - return ( - - - - {/* Left: Source list — fixed 400px */} - - - - {/* Right: Detail panel */} - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/supply/DataQuality.tsx b/.claude/worktrees/agent-a82a3716/src/pages/supply/DataQuality.tsx deleted file mode 100644 index 105661d..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/supply/DataQuality.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { useState } from 'react' -import { - Box, - Card, - Chip, - LinearProgress, - MenuItem, - Select, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Tooltip, - Typography, -} from '@mui/material' -import { useQuery } from '@tanstack/react-query' -import { ErrorState, LoadingPage, SectionContainer } from '../../components/ui' -import { DataQualityBadge, FreshnessIndicator } from '../../components/data-quality' -import { propertyService } from '../../services/propertyService' -import { getRecommendedActions } from '../../services/dataQualityService' -import { DataFreshness } from '../../domain/enums' - -type QualityFilter = '' | 'HIGH' | 'MEDIUM' | 'LOW' | 'INCOMPLETE' - -function getQualityColor(score: number): 'success' | 'warning' | 'error' { - if (score >= 0.8) return 'success' - if (score >= 0.6) return 'warning' - return 'error' -} - -export default function DataQuality() { - const [qualityFilter, setQualityFilter] = useState('') - - 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) - - const filtered = [...properties] - .filter(p => { - if (!qualityFilter) return true - if (qualityFilter === 'INCOMPLETE') return p.dataQuality.missingCriticalFields.length > 0 - if (qualityFilter === 'HIGH') return p.dataQuality.score >= 0.8 && p.dataQuality.missingCriticalFields.length === 0 - if (qualityFilter === 'MEDIUM') return p.dataQuality.score >= 0.6 && p.dataQuality.score < 0.8 - if (qualityFilter === 'LOW') return p.dataQuality.score < 0.6 - return true - }) - .sort((a, b) => a.dataQuality.score - b.dataQuality.score) - - return ( - - {/* Page Header */} - - Datenpflege - Vollständigkeit, Aktualität und Vertrauen der Objektdaten - - - - - {/* Summary Stats */} - - - Ø Qualitätsscore - = 0.8 ? '#1a7a4a' : avgScore >= 0.6 ? '#d97706' : '#c0392b' }}> - {Math.round(avgScore * 100)}% - - - - - - Pflichtfelder fehlen - 2 ? '#c0392b' : criticalIssues.length > 0 ? '#d97706' : '#1a7a4a' }}> - {criticalIssues.length} - - von {properties.length} Objekten - - - - Veraltete Daten - 2 ? '#c0392b' : staleData.length > 0 ? '#d97706' : '#1a7a4a' }}> - {staleData.length} - - von {properties.length} Objekten - - - - {/* Quality Distribution */} - - - - {[ - { label: 'Hoch (≥80%)', count: highQuality.length, color: 'success' as const }, - { label: 'Mittel (60–79%)', count: medQuality.length, color: 'warning' as const }, - { label: 'Niedrig (<60%)', count: lowQuality.length, color: 'error' as const }, - ].map(row => ( - - {row.label} - - - 0 ? (row.count / properties.length) * 100 : 0} - color={row.color} - sx={{ height: 10, borderRadius: 5 }} - /> - - - {properties.length > 0 ? Math.round((row.count / properties.length) * 100) : 0}% - - - ))} - - - - - {/* Objects Table */} - - {/* Filter bar */} - - - - {filtered.length} von {properties.length} Objekten - - - - - - - - Objekt - Qualität - Aktualität - Pflichtfelder - Nächste Massnahme - - - - {filtered.map(property => { - const q = property.dataQuality - const hasCritical = q.missingCriticalFields.length > 0 - const actions = getRecommendedActions(q, q.freshness) - const topAction = actions[0] ?? null - - return ( - - - {property.title} - {property.location.city} - - - - - - - - - - - - {q.missingCriticalFields.length === 0 ? ( - - ) : ( - - - - )} - - - - {topAction ? ( - - - {topAction.label} - - - {topAction.detail} - - - ) : ( - - )} - - - ) - })} - -
-
-
- -
-
- ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/supply/FutureAvailability.tsx b/.claude/worktrees/agent-a82a3716/src/pages/supply/FutureAvailability.tsx deleted file mode 100644 index 63fffe9..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/supply/FutureAvailability.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { useState } from 'react' -import { Box, Chip, Drawer, Typography } from '@mui/material' -import { useFutureSignals } from '../../hooks/useFutureSignals' -import { - FutureSignalCard, - FutureSignalFilterBar, - FutureSignalDetailPanel, - FutureSignalEmptyState, - DEFAULT_SIGNAL_FILTERS, -} from '../../components/future-signals' -import { AddToShortlistDialog } from '../../components/shortlist' -import type { SignalFilterState } from '../../components/future-signals' -import type { FutureSignal } from '../../domain/futureSignal' - -function applyFilters(signals: FutureSignal[], f: SignalFilterState): FutureSignal[] { - return signals.filter(s => { - if (f.signalType && s.signalType !== f.signalType) return false - if (f.minConfidence > 0 && s.confidenceScore < f.minConfidence) return false - if (f.sensitivityLevel && s.sensitivityLevel !== f.sensitivityLevel) return false - if (f.reviewStatus) { - const rs = s.reviewStatus ?? 'UNREVIEWED' - if (rs !== f.reviewStatus) return false - } - if (f.timeHorizon === 'short' && s.timeHorizonMonths > 6) return false - if (f.timeHorizon === 'medium' && (s.timeHorizonMonths <= 6 || s.timeHorizonMonths > 12)) return false - if (f.timeHorizon === 'long' && s.timeHorizonMonths <= 12) return false - return true - }) -} - -export default function FutureAvailability() { - const { data: signals = [], isLoading } = useFutureSignals() - const [filters, setFilters] = useState(DEFAULT_SIGNAL_FILTERS) - const [selectedSignal, setSelectedSignal] = useState(null) - - const filtered = applyFilters(signals, filters) - - const needsReviewCount = signals.filter(s => !s.reviewStatus || s.reviewStatus === 'UNREVIEWED').length - const highConfCount = signals.filter(s => s.confidenceScore >= 0.75).length - const confidentialCount = signals.filter(s => s.sensitivityLevel === 'CONFIDENTIAL').length - - function handleSelect(signal: FutureSignal) { - setSelectedSignal(prev => prev?.id === signal.id ? null : signal) - } - - return ( - - - - {/* Header */} - - - - Zukunftssignale - Probabilistische Markt- und Verfügbarkeitssignale - - - - - - 0 ? '#fef3c7' : '#f1f5f9', color: needsReviewCount > 0 ? '#d97706' : '#475569', fontWeight: needsReviewCount > 0 ? 600 : 400 }} - /> - - {confidentialCount > 0 && ( - - )} - - - - {/* Filter bar */} - - - {/* Signal list — always full width */} - - {isLoading ? ( - - {[0, 1, 2, 3].map(i => ( - - ))} - - ) : filtered.length === 0 ? ( - - ) : ( - - {filtered.map(signal => ( - - ))} - - )} - - - {/* Detail drawer */} - setSelectedSignal(null)} - slotProps={{ paper: { sx: { width: 650, boxShadow: '-4px 0 24px rgba(0,0,0,0.10)' } } }} - > - {selectedSignal && ( - setSelectedSignal(null)} - /> - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/supply/MatchCenter.tsx b/.claude/worktrees/agent-a82a3716/src/pages/supply/MatchCenter.tsx deleted file mode 100644 index 2dded20..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/supply/MatchCenter.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { useMemo, useState } from 'react' -import { - Box, - Chip, - Drawer, - IconButton, - MenuItem, - Select, - Typography, -} from '@mui/material' -import { X } from 'lucide-react' -import { useMatches, useApproveMatch } from '../../hooks/useMatches' -import { useProperties } from '../../hooks/useProperties' -import { useNeeds } from '../../hooks/useNeeds' -import { useMatchCenterStore } from '../../stores/matchCenterStore' -import { useToastStore } from '../../stores/toastStore' -import { MatchListCard, MatchBriefingPanel, MatchCenterSkeleton } from '../../components/match-center' -import type { Match } from '../../domain/match' - -const STRENGTH_OPTIONS = [ - { value: '', label: 'Alle Stärken' }, - { value: 'STRONG', label: 'Stark (≥80)' }, - { value: 'MODERATE', label: 'Mittel (60–79)' }, - { value: 'WEAK', label: 'Schwach (<60)' }, -] - -const STATUS_OPTIONS = [ - { value: '', label: 'Alle Status' }, - { value: 'PENDING_REVIEW', label: 'Ausstehend' }, - { value: 'APPROVED', label: 'Genehmigt' }, - { value: 'REJECTED', label: 'Abgelehnt' }, -] - -export default function MatchCenter() { - const { data: matches = [], isLoading } = useMatches() - const { data: properties = [] } = useProperties() - const { data: needs = [] } = useNeeds() - const { setSelectedProperty, setSelectedNeed } = useMatchCenterStore() - const approveMatch = useApproveMatch() - const showToast = useToastStore((s) => s.showToast) - - const [selectedMatchId, setSelectedMatchId] = useState(null) - const [filterStrength, setFilterStrength] = useState('') - const [filterStatus, setFilterStatus] = useState('') - - const propMap = useMemo(() => new Map(properties.map(p => [p.id, p])), [properties]) - const needMap = useMemo(() => new Map(needs.map(n => [n.id, n])), [needs]) - - const filtered = useMemo(() => { - return matches - .filter(m => { - if (filterStrength && m.matchStrength !== filterStrength) return false - if (filterStatus && m.status !== filterStatus) return false - return true - }) - .sort((a, b) => b.matchScore - a.matchScore) - }, [matches, filterStrength, filterStatus]) - - const strongCount = matches.filter(m => m.matchScore >= 80).length - const pendingCount = matches.filter(m => m.status === 'PENDING_REVIEW').length - - function handleSelectMatch(match: Match) { - setSelectedMatchId(match.id) - setSelectedProperty(match.propertyId) - setSelectedNeed(match.needId) - } - - function handleCloseDrawer() { - setSelectedMatchId(null) - setSelectedProperty(null) - setSelectedNeed(null) - } - - return ( - - - {/* Header */} - - - Match Center - Automatisch berechnete Matches - - - - - {pendingCount > 0 && ( - - )} - - - - {/* Filter bar */} - - - - - {filtered.length} von {matches.length} Matches - - - - {/* Match list */} - - {isLoading ? ( - - ) : filtered.length === 0 ? ( - - Keine Matches für diese Filter. - - ) : ( - - {filtered.map(match => ( - handleSelectMatch(match)} - onApprove={() => approveMatch.mutate(match.id, { - onSuccess: () => showToast('Match genehmigt.'), - onError: () => showToast('Genehmigung fehlgeschlagen.', 'error'), - })} - /> - ))} - - )} - - - {/* Detail Drawer */} - - - - Match-Briefing - - - - - - - - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/supply/Properties.tsx b/.claude/worktrees/agent-a82a3716/src/pages/supply/Properties.tsx deleted file mode 100644 index cdc805b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/supply/Properties.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { useState } from 'react' -import { Box, Drawer } from '@mui/material' -import { useNavigate } from 'react-router' -import { PageHeader } from '../../components/layout' -import { DecisionContextPanel } from '../../components/ui' -import { useProperties } from '../../hooks/useProperties' -import { PropertyFilterBar, PropertyTable, PropertyDetailView } from '../../components/supply' -import type { PropertyTableFilters } from '../../components/supply' -import type { Property } from '../../domain/property' - -function applyFilters(properties: Property[], filters: PropertyTableFilters): Property[] { - let result = [...properties] - - if (filters.search) { - const q = filters.search.toLowerCase() - result = result.filter( - p => - p.title.toLowerCase().includes(q) || - p.location.city.toLowerCase().includes(q) || - p.address.street.toLowerCase().includes(q), - ) - } - - if (filters.assetTypes && filters.assetTypes.length > 0) { - result = result.filter(p => filters.assetTypes!.includes(p.assetType)) - } - - if (filters.availabilityStatus) { - result = result.filter(p => p.availabilityStatus === filters.availabilityStatus) - } - - if (filters.sortBy) { - const dir = filters.sortDir === 'asc' ? 1 : -1 - result.sort((a, b) => { - switch (filters.sortBy) { - case 'dataQuality': return dir * (a.dataQuality.score - b.dataQuality.score) - case 'area': return dir * (a.areaSqm - b.areaSqm) - case 'rent': return dir * (a.rentPricePerSqm - b.rentPricePerSqm) - case 'confidence': return dir * (a.confidenceScore - b.confidenceScore) - case 'availability': return dir * a.availabilityStatus.localeCompare(b.availabilityStatus) - default: return 0 - } - }) - } - - return result -} - -export default function Properties() { - const navigate = useNavigate() - const [selectedId, setSelectedId] = useState(null) - const [filters, setFilters] = useState({}) - - const { data: properties = [], isLoading, isError } = useProperties() - const filtered = applyFilters(properties, filters) - - // Decision-relevant aggregates - const matchReady = properties.filter( - p => (p.availabilityStatus === 'AVAILABLE_NOW' || p.availabilityStatus === 'AVAILABLE_SOON') && - p.confidenceScore >= 0.7 && p.dataQuality.missingCriticalFields.length === 0 - ) - const criticalGaps = properties.filter(p => p.dataQuality.missingCriticalFields.length > 0) - const lowConfidence = properties.filter(p => p.confidenceScore < 0.55) - const staleOrOutdated = properties.filter( - p => p.dataQuality.freshness === 'STALE' || p.dataQuality.freshness === 'OUTDATED' - ) - - // Unique missing fields across all objects - const allMissingFields = [...new Set( - properties.flatMap(p => p.dataQuality.missingCriticalFields) - )].slice(0, 4) - - return ( - - - - {!isLoading && properties.length > 0 && ( - 0 ? 'positive' : 'warning' }, - ...(criticalGaps.length > 0 - ? [{ label: 'kritische Datenlücken', value: criticalGaps.length, severity: 'critical' as const }] - : [] - ), - ...(lowConfidence.length > 0 - ? [{ label: 'Konfidenz < 55%', value: lowConfidence.length, severity: 'warning' as const }] - : [] - ), - ...(staleOrOutdated.length > 0 - ? [{ label: 'veraltete Daten', value: staleOrOutdated.length, severity: 'warning' as const }] - : [] - ), - ]} - missing={allMissingFields.length > 0 - ? [`Fehlende Pflichtfelder bei ${criticalGaps.length} Objekten: ${allMissingFields.join(', ')}`] - : [] - } - risks={[ - ...(criticalGaps.length > 0 - ? [`${criticalGaps.length} Objekte werden potenziellen Mietern nicht angezeigt`] - : [] - ), - ...(staleOrOutdated.length > 0 - ? [`${staleOrOutdated.length} Objekte mit veralteten Preisen oder Verfügbarkeiten`] - : [] - ), - ]} - actions={[ - { - label: 'Datenpflege starten', - primary: criticalGaps.length > 0 || staleOrOutdated.length > 0, - onClick: () => navigate('/supply/data-quality'), - }, - ]} - /> - )} - - - - - - - - setSelectedId(null)} - slotProps={{ paper: { sx: { width: 650, boxShadow: '-4px 0 24px rgba(0,0,0,0.10)' } } }} - > - {selectedId && ( - setSelectedId(null)} /> - )} - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/pages/supply/SupplyDashboard.tsx b/.claude/worktrees/agent-a82a3716/src/pages/supply/SupplyDashboard.tsx deleted file mode 100644 index ed5cb7f..0000000 --- a/.claude/worktrees/agent-a82a3716/src/pages/supply/SupplyDashboard.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { Box, Alert, Button, Typography } from '@mui/material' -import { useNavigate } from 'react-router' -import { useSupplyDashboard } from '../../hooks/useSupplyDashboard' -import { useSessionStore } from '../../stores/sessionStore' -import { UserRole } from '../../domain/enums' -import { - DashboardHeader, - KpiGrid, - StrongMatchOverview, - DataQualityWidget, - FutureSignalWidget, - ReviewTaskWidget, - QuickActionPanel, - DashboardSkeleton, -} from '../../components/supply' - -function WidgetError({ label }: { label: string }) { - return ( - - {label} konnte nicht geladen werden. - - ) -} - -export default function SupplyDashboard() { - const { data, isLoading, isError, refetch } = useSupplyDashboard() - const { currentUser } = useSessionStore() - const navigate = useNavigate() - - const role = currentUser?.role ?? UserRole.DEMAND_USER - const isReviewer = role === UserRole.REVIEWER - const isOwnerViewer = role === UserRole.OWNER_VIEWER - const canSeeMatches = role !== UserRole.OWNER_VIEWER && role !== UserRole.DEMAND_USER - const canSeeOperations = - role === UserRole.SUPER_ADMIN || - role === UserRole.ORGANIZATION_ADMIN || - role === UserRole.REVIEWER - - if (isLoading) return - - if (isError) { - return ( - - - Dashboard konnte nicht geladen werden - - - Der Service ist vorübergehend nicht verfügbar. - - - - ) - } - - if (!data || data.totalProperties === 0) { - return ( - - Noch keine Objekte vorhanden - - Fügen Sie Ihr erstes Objekt hinzu oder laden Sie Demo-Daten. - - - - ) - } - - return ( - - - - {isOwnerViewer ? ( - - - - Aktive Objekte - - - {data.activeProperties} / {data.totalProperties} - - - - - Ø Datenqualität - - - {data.avgDataQuality}% - - - - ) : ( - - )} - - {/* REVIEWER: Review Queue first, then Matches */} - {isReviewer && ( - - {data.reviewTasks !== null ? ( - navigate('/ops/review-queue')} - /> - ) : ( - - )} - {canSeeMatches && - (data.strongMatches !== null ? ( - navigate('/supply/match-center')} - /> - ) : ( - - ))} - - )} - - {/* Default: Matches + Data Quality */} - {!isReviewer && canSeeMatches && ( - - {data.strongMatches !== null ? ( - navigate('/supply/match-center')} - /> - ) : ( - - )} - {data.dataQuality !== null ? ( - navigate('/supply/data-quality')} - /> - ) : ( - - )} - - )} - - {canSeeMatches && data.strongMatches?.length === 0 && ( - navigate('/demand/ai-search')}> - Bedarfsprofil erstellen - - } - > - Noch keine Matches vorhanden. Erstellen Sie ein Bedarfsprofil, um passende Objekte zu - finden. - - )} - - {!isOwnerViewer && ( - - {data.futureSignals !== null ? ( - - ) : ( - - )} - {canSeeOperations && !isReviewer ? ( - data.reviewTasks !== null ? ( - navigate('/ops/review-queue')} - /> - ) : ( - - ) - ) : !canSeeOperations && data.dataQuality !== null ? ( - navigate('/supply/data-quality')} - /> - ) : null} - - )} - - - - ) -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/AuthProvider.tsx b/.claude/worktrees/agent-a82a3716/src/provider/AuthProvider.tsx deleted file mode 100644 index c69d396..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/AuthProvider.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { createContext, useContext, type ReactNode } from 'react' -import { useSessionStore } from '../stores/sessionStore' -import type { MockUser } from '../stores/sessionStore' - -// Placeholder AuthContext — swap for real auth (Supabase, Auth0, etc.) later. -// All call sites use this context; no component imports sessionStore directly. - -interface AuthContextValue { - user: MockUser | null - isAuthenticated: boolean - isLoading: boolean - login: (user: MockUser) => void - logout: () => void -} - -const AuthContext = createContext(null) - -export function AuthProvider({ children }: { children: ReactNode }) { - const { currentUser, isAuthenticated, login, logout } = useSessionStore() - - const value: AuthContextValue = { - user: currentUser, - isAuthenticated, - isLoading: false, // always resolved in mock mode - login, - logout, - } - - return {children} -} - -export function useAuth(): AuthContextValue { - const ctx = useContext(AuthContext) - if (!ctx) throw new Error('useAuth must be used within AuthProvider') - return ctx -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/IAIMonitoringProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/IAIMonitoringProvider.ts deleted file mode 100644 index 2e40a43..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/IAIMonitoringProvider.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { AIOutput, AIOutputType } from '../domain/aiOutput' -import type { ReviewStatus } from '../domain/enums' - -export interface AIMonitoringFilters { - type?: AIOutputType - reviewStatus?: ReviewStatus - hasError?: boolean - model?: string -} - -export interface IAIMonitoringProvider { - getOutputs(filters?: AIMonitoringFilters): Promise - getOutput(id: string): Promise - updateReviewStatus(id: string, status: ReviewStatus): Promise -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/IDashboardProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/IDashboardProvider.ts deleted file mode 100644 index c08ad70..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/IDashboardProvider.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface DashboardStats { - totalProperties: number - verifiedProperties: number - externalMarketProperties: number - futureSignalProperties: number - totalMatches: number - pendingReviews: number - approvedMatches: number - activeNeeds: number - totalSignals: number - verifiedSignals: number - averageMatchScore: number - highConfidenceMatches: number -} - -export interface IDashboardProvider { - getStats(organizationId?: string): Promise -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/IDataSourceProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/IDataSourceProvider.ts deleted file mode 100644 index c9907f3..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/IDataSourceProvider.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { DataSource, ConnectorRun, SourceStatus, TermsStatus, SourceFilters } from '../domain/dataSource' - -export interface IDataSourceProvider { - getSources(filters?: SourceFilters): Promise - getSource(id: string): Promise - getConnectorRuns(sourceId: string): Promise - triggerMockRun(sourceId: string): Promise - updateSourceStatus(id: string, status: SourceStatus): Promise - markTermsStatus(id: string, termsStatus: TermsStatus): Promise -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/IFutureSignalProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/IFutureSignalProvider.ts deleted file mode 100644 index 57a8c31..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/IFutureSignalProvider.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { FutureSignal } from '../domain/futureSignal' -import type { SignalType, ReviewStatus } from '../domain/enums' - -export interface FutureSignalFilters { - signalType?: SignalType - minProbability?: number - organizationId?: string - isVerified?: boolean - sensitivityLevel?: string - reviewStatus?: ReviewStatus - maxTimeHorizonMonths?: number -} - -export interface IFutureSignalProvider { - getAll(filters?: FutureSignalFilters): Promise - getById(id: string): Promise - getByProperty(propertyId: string): Promise - verify(id: string, verifiedBy: string): Promise - updateReviewStatus(id: string, status: ReviewStatus): Promise -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/IMarketIntelligenceProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/IMarketIntelligenceProvider.ts deleted file mode 100644 index 3c59fef..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/IMarketIntelligenceProvider.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { MarketSignal, MarketSignalFilters, SignalProcessingStatus } from '../domain/marketSignal' - -export interface IMarketIntelligenceProvider { - getSignals(filters?: MarketSignalFilters): Promise - getSignalById(id: string): Promise - updateSignalStatus(id: string, status: SignalProcessingStatus): Promise - convertToFutureSignal(id: string): Promise<{ futureSignalId: string }> - linkSignalToEntity( - id: string, - entityType: 'property' | 'need', - entityId: string, - ): Promise -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/IMatchProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/IMatchProvider.ts deleted file mode 100644 index 928335e..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/IMatchProvider.ts +++ /dev/null @@ -1,18 +0,0 @@ -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/.claude/worktrees/agent-a82a3716/src/provider/INeedProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/INeedProvider.ts deleted file mode 100644 index 08a2168..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/INeedProvider.ts +++ /dev/null @@ -1,16 +0,0 @@ -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/.claude/worktrees/agent-a82a3716/src/provider/IPropertyProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/IPropertyProvider.ts deleted file mode 100644 index 4401a36..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/IPropertyProvider.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/.claude/worktrees/agent-a82a3716/src/provider/IReviewProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/IReviewProvider.ts deleted file mode 100644 index 84c44f2..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/IReviewProvider.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ReviewTask, ReviewPriority, ReviewTaskStatus, ReviewEntityType, ReviewNote } from '../domain/review' - -export interface ReviewFilters { - priority?: ReviewPriority - status?: ReviewTaskStatus - entityType?: ReviewEntityType - assignedTo?: string - organizationId?: string -} - -export interface IReviewProvider { - getQueue(filters?: ReviewFilters): Promise - getById(id: string): Promise - updateStatus(id: string, status: ReviewTaskStatus, userId: string, note?: string): Promise - addNote(id: string, note: Omit): Promise - assign(id: string, assignTo: string): Promise - // Legacy actions (delegate to updateStatus internally) - approve(id: string, reviewedBy: string, notes?: string): Promise - reject(id: string, reviewedBy: string, notes?: string): Promise -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/IShortlistProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/IShortlistProvider.ts deleted file mode 100644 index eff3d14..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/IShortlistProvider.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Shortlist, CreateShortlistInput, UpdateShortlistInput, ShortlistItemInput } from '../domain/shortlist' -import type { ShortlistStatus } from '../domain/enums' - -export interface ShortlistFilters { - needId?: string - status?: ShortlistStatus - createdBy?: string - organizationId?: string -} - -export interface IShortlistProvider { - getAll(filters?: ShortlistFilters): Promise - getById(id: string): Promise - create(data: CreateShortlistInput): Promise - update(id: string, data: UpdateShortlistInput): Promise - addItem(id: string, item: ShortlistItemInput): Promise - removeItem(id: string, resultId: string): Promise - remove(id: string): Promise -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/ISignalPipelineProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/ISignalPipelineProvider.ts deleted file mode 100644 index 7b4d48b..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/ISignalPipelineProvider.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { PipelineState, AuditTrailEntry, GateType } from '../domain/signalPipeline' - -export interface ISignalPipelineProvider { - getPipelineState(signalId: string): Promise - evaluateGate(signalId: string, gateType: GateType): Promise - getAuditTrail(signalId: string): Promise - publishToFutureAvailability(signalId: string): Promise -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/MockupAIMonitoringProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/MockupAIMonitoringProvider.ts deleted file mode 100644 index 79672fa..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/MockupAIMonitoringProvider.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { IAIMonitoringProvider, AIMonitoringFilters } from './IAIMonitoringProvider' -import type { AIOutput } from '../domain/aiOutput' -import type { ReviewStatus } from '../domain/enums' -import { mockAIOutputs } from '../mock-data/aiOutputs' - -const store: AIOutput[] = [...mockAIOutputs] - -export const MockupAIMonitoringProvider: IAIMonitoringProvider = { - async getOutputs(filters?: AIMonitoringFilters) { - let results = [...store] - if (filters?.type) results = results.filter(o => o.type === filters.type) - if (filters?.reviewStatus) results = results.filter(o => o.reviewStatus === filters.reviewStatus) - if (filters?.hasError === true) results = results.filter(o => !!o.error) - if (filters?.hasError === false) results = results.filter(o => !o.error) - if (filters?.model) results = results.filter(o => o.model === filters.model) - return results.sort((a, b) => b.createdAt.localeCompare(a.createdAt)) - }, - - async getOutput(id: string) { - return store.find(o => o.id === id) ?? null - }, - - async updateReviewStatus(id: string, status: ReviewStatus): Promise { - const idx = store.findIndex(o => o.id === id) - if (idx === -1) throw new Error(`AIOutput ${id} not found`) - store[idx] = { ...store[idx], reviewStatus: status } - return store[idx] - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/MockupDashboardProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/MockupDashboardProvider.ts deleted file mode 100644 index 2941522..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/MockupDashboardProvider.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { IDashboardProvider, DashboardStats } from './IDashboardProvider' -import { mockProperties } from '../mock-data/properties' -import { mockMatches } from '../mock-data/matches' -import { mockNeeds } from '../mock-data/needs' -import { mockFutureSignals } from '../mock-data/futureSignals' -import { mockReviewQueue } from '../mock-data/reviewQueue' -import { mockDelay } from '../lib/mockUtils' - -export const MockupDashboardProvider: IDashboardProvider = { - async getStats(organizationId?) { - await mockDelay() - - let props = mockProperties - let matches = mockMatches - let needs = mockNeeds - let signals = mockFutureSignals - let queue = mockReviewQueue - - if (organizationId) { - props = props.filter(p => p.organizationId === organizationId) - matches = matches.filter(m => m.organizationId === organizationId) - needs = needs.filter(n => n.organizationId === organizationId) - signals = signals.filter(s => s.organizationId === organizationId) - queue = queue.filter(r => r.relatedOrganizationId === organizationId) - } - - const avgScore = - matches.length > 0 - ? matches.reduce((sum, m) => sum + m.matchScore, 0) / matches.length - : 0 - - const stats: DashboardStats = { - totalProperties: props.length, - verifiedProperties: props.filter(p => p.resultType === 'VERIFIED_PORTFOLIO').length, - externalMarketProperties: props.filter(p => p.resultType === 'EXTERNAL_MARKET').length, - futureSignalProperties: props.filter(p => p.resultType === 'FUTURE_AVAILABILITY').length, - totalMatches: matches.length, - pendingReviews: queue.filter(r => r.status === 'PENDING').length, - approvedMatches: matches.filter(m => m.isApproved === true).length, - activeNeeds: needs.length, - totalSignals: signals.length, - verifiedSignals: signals.filter(s => s.isVerified).length, - averageMatchScore: Math.round(avgScore), - highConfidenceMatches: matches.filter(m => m.confidenceLevel >= 0.75).length, - } - return stats - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/MockupDataSourceProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/MockupDataSourceProvider.ts deleted file mode 100644 index 66c9c83..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/MockupDataSourceProvider.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { IDataSourceProvider } from './IDataSourceProvider' -import type { DataSource, ConnectorRun, SourceStatus, TermsStatus, SourceFilters } from '../domain/dataSource' -import { ConnectorRunStatus } from '../domain/dataSource' -import { MOCK_DATA_SOURCES, MOCK_CONNECTOR_RUNS } from '../mock-data/dataSources' - -let sources: DataSource[] = [...MOCK_DATA_SOURCES] -let runs: ConnectorRun[] = [...MOCK_CONNECTOR_RUNS] - -function applyFilters(items: DataSource[], filters?: SourceFilters): DataSource[] { - if (!filters) return items - return items.filter((s) => { - if (filters.sourceType && s.sourceType !== filters.sourceType) return false - if (filters.status && s.status !== filters.status) return false - if (filters.termsStatus && s.termsStatus !== filters.termsStatus) return false - if (filters.search) { - const q = filters.search.toLowerCase() - if (!s.name.toLowerCase().includes(q) && !s.legalBasis.toLowerCase().includes(q)) return false - } - return true - }) -} - -export const MockupDataSourceProvider: IDataSourceProvider = { - async getSources(filters?: SourceFilters) { - return applyFilters([...sources], filters) - }, - - async getSource(id: string) { - return sources.find((s) => s.id === id) ?? null - }, - - async getConnectorRuns(sourceId: string) { - return runs - .filter((r) => r.sourceId === sourceId) - .sort((a, b) => b.startedAt.localeCompare(a.startedAt)) - }, - - async triggerMockRun(sourceId: string) { - const source = sources.find((s) => s.id === sourceId) - const now = new Date().toISOString() - const newRun: ConnectorRun = { - id: `run-${crypto.randomUUID().slice(0, 8)}`, - sourceId, - startedAt: now, - finishedAt: now, - status: ConnectorRunStatus.COMPLETED, - itemsDetected: Math.floor(Math.random() * 200) + 50, - itemsNormalized: Math.floor(Math.random() * 180) + 40, - itemsRejected: Math.floor(Math.random() * 15), - signalsCreated: Math.floor(Math.random() * 8) + 1, - errors: [], - warnings: [], - runSummary: `Demo-Run für ${source?.name ?? sourceId} erfolgreich abgeschlossen.`, - } - runs = [newRun, ...runs] - sources = sources.map((s) => - s.id === sourceId ? { ...s, lastRunAt: now } : s - ) - return newRun - }, - - async updateSourceStatus(id: string, status: SourceStatus) { - const idx = sources.findIndex((s) => s.id === id) - if (idx === -1) throw new Error(`Source ${id} not found`) - sources[idx] = { ...sources[idx], status } - return sources[idx] - }, - - async markTermsStatus(id: string, termsStatus: TermsStatus) { - const idx = sources.findIndex((s) => s.id === id) - if (idx === -1) throw new Error(`Source ${id} not found`) - sources[idx] = { ...sources[idx], termsStatus } - return sources[idx] - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/MockupFutureSignalProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/MockupFutureSignalProvider.ts deleted file mode 100644 index 1143780..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/MockupFutureSignalProvider.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { IFutureSignalProvider, FutureSignalFilters } from './IFutureSignalProvider' -import type { FutureSignal } from '../domain/futureSignal' -import type { ReviewStatus } from '../domain/enums' -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) - if (filters?.sensitivityLevel) results = results.filter(s => s.sensitivityLevel === filters.sensitivityLevel) - if (filters?.reviewStatus) results = results.filter(s => (s.reviewStatus ?? 'UNREVIEWED') === filters.reviewStatus) - if (filters?.maxTimeHorizonMonths !== undefined) results = results.filter(s => s.timeHorizonMonths <= filters.maxTimeHorizonMonths!) - 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] - }, - async updateReviewStatus(id, status: ReviewStatus) { - const idx = store.findIndex(s => s.id === id) - store[idx] = { ...store[idx], reviewStatus: status, updatedAt: new Date().toISOString() } - return store[idx] - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/MockupMarketIntelligenceProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/MockupMarketIntelligenceProvider.ts deleted file mode 100644 index 52cd866..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/MockupMarketIntelligenceProvider.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { IMarketIntelligenceProvider } from './IMarketIntelligenceProvider' -import type { MarketSignal, MarketSignalFilters, SignalProcessingStatus } from '../domain/marketSignal' -import { MOCK_MARKET_SIGNALS } from '../mock-data/marketSignals' - -// Mutable in-memory copy for status updates -let signals: MarketSignal[] = [...MOCK_MARKET_SIGNALS] - -function applyFilters(data: MarketSignal[], filters?: MarketSignalFilters): MarketSignal[] { - if (!filters) return data - return data.filter((s) => { - if (filters.sourceCategory && s.sourceCategory !== filters.sourceCategory) return false - if (filters.processingStatus && s.processingStatus !== filters.processingStatus) return false - if (filters.sensitivityLevel && s.sensitivityLevel !== filters.sensitivityLevel) return false - if (filters.signalType && s.signalType !== filters.signalType) return false - if (filters.search) { - const q = filters.search.toLowerCase() - const match = s.title.toLowerCase().includes(q) - || s.summary.toLowerCase().includes(q) - || s.location.toLowerCase().includes(q) - if (!match) return false - } - return true - }) -} - -export const MockupMarketIntelligenceProvider: IMarketIntelligenceProvider = { - async getSignals(filters?: MarketSignalFilters): Promise { - return applyFilters(signals, filters) - }, - - async getSignalById(id: string): Promise { - return signals.find((s) => s.id === id) ?? null - }, - - async updateSignalStatus(id: string, status: SignalProcessingStatus): Promise { - const idx = signals.findIndex((s) => s.id === id) - if (idx === -1) throw new Error(`Signal ${id} not found`) - signals[idx] = { ...signals[idx], processingStatus: status, updatedAt: new Date().toISOString() } - return signals[idx] - }, - - async convertToFutureSignal(id: string): Promise<{ futureSignalId: string }> { - const futureSignalId = `fs-${id}-${Date.now()}` - const idx = signals.findIndex((s) => s.id === id) - if (idx !== -1) { - signals[idx] = { - ...signals[idx], - processingStatus: 'CONVERTED_TO_FUTURE_AVAILABILITY', - possibleFutureSignalId: futureSignalId, - updatedAt: new Date().toISOString(), - } - } - return { futureSignalId } - }, - - async linkSignalToEntity( - id: string, - entityType: 'property' | 'need', - entityId: string, - ): Promise { - const idx = signals.findIndex((s) => s.id === id) - if (idx === -1) throw new Error(`Signal ${id} not found`) - signals[idx] = { - ...signals[idx], - ...(entityType === 'property' ? { linkedPropertyId: entityId } : { linkedNeedId: entityId }), - updatedAt: new Date().toISOString(), - } - return signals[idx] - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/MockupMatchProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/MockupMatchProvider.ts deleted file mode 100644 index ce83f21..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/MockupMatchProvider.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { IMatchProvider, MatchFilters } from './IMatchProvider' -import type { Match } from '../domain/match' -import { mockMatches } from '../mock-data/matches' - -export const matchStore: Match[] = [...mockMatches] -const store = matchStore - -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/.claude/worktrees/agent-a82a3716/src/provider/MockupNeedProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/MockupNeedProvider.ts deleted file mode 100644 index 552205a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/MockupNeedProvider.ts +++ /dev/null @@ -1,151 +0,0 @@ -import type { INeedProvider, NeedFilters } from './INeedProvider' -import type { Need, CreateNeedInput, UpdateNeedInput } from '../domain/need' -import { mockNeeds } from '../mock-data/needs' -import { matchStore } from './MockupMatchProvider' -import { propertyStore } from './MockupPropertyProvider' -import { MatchStrength, MatchStatus, RiskLevel } from '../domain/enums' -import type { Match } from '../domain/match' - -const store: Need[] = [...mockNeeds] - -// ── Location scoring ─────────────────────────────────────────────────────────── - -const CANTON_MAP: Record = { - zürich: 'zh', zug: 'zg', winterthur: 'zh', uster: 'zh', bülach: 'zh', oerlikon: 'zh', - bern: 'be', biel: 'be', thun: 'be', köniz: 'be', - basel: 'bs', muttenz: 'bl', pratteln: 'bl', reinach: 'bl', allschwil: 'bl', binningen: 'bl', - genf: 'ge', genève: 'ge', carouge: 'ge', lancy: 'ge', - 'st. gallen': 'sg', 'st.gallen': 'sg', rapperswil: 'sg', -} - -function locationScore(propCity: string, preferredLocations: string[]): number { - const pc = propCity.toLowerCase() - for (const pref of preferredLocations) { - const p = pref.toLowerCase() - if (pc.includes(p) || p.includes(pc)) return 1.0 - } - // Same canton check - const propCanton = CANTON_MAP[pc] - if (propCanton) { - for (const pref of preferredLocations) { - const prefCanton = CANTON_MAP[pref.toLowerCase()] - if (prefCanton && prefCanton === propCanton) return 0.55 - } - } - return 0.30 -} - -function computeScore(prop: { assetType: string; areaSqm: number; rentPricePerSqm: number; location: { city: string } }, need: Need): number | null { - if (need.assetType && prop.assetType !== need.assetType) return null - - const locScore = locationScore(prop.location.city, need.preferredLocations ?? []) - - // Location dominates: same city → 50-90 base, different → 25-45 - let score = locScore >= 0.9 ? 65 : locScore >= 0.5 ? 42 : 28 - - // Area overlap (+0-20) - if (need.requiredArea && prop.areaSqm) { - const { min, max } = need.requiredArea - if (prop.areaSqm >= min && prop.areaSqm <= max) score += 20 - else if (prop.areaSqm >= min * 0.7 && prop.areaSqm <= max * 1.5) score += 10 - else if (prop.areaSqm < min * 0.5 || prop.areaSqm > max * 2) score -= 10 - } - - // Budget fit (+0-10) - if (need.budgetRange?.maxPerSqm && prop.rentPricePerSqm) { - if (prop.rentPricePerSqm <= need.budgetRange.maxPerSqm) score += 10 - else if (prop.rentPricePerSqm <= need.budgetRange.maxPerSqm * 1.2) score += 3 - else score -= 8 - } - - // Small jitter so results look natural - score += Math.floor(Math.random() * 6) - 2 - - return Math.min(97, Math.max(22, score)) -} - -function strengthFromScore(s: number): string { - if (s >= 75) return MatchStrength.STRONG - if (s >= 55) return MatchStrength.MODERATE - return MatchStrength.WEAK -} - -function generateSyntheticMatches(need: Need) { - const now = new Date().toISOString() - - for (const prop of propertyStore) { - const score = computeScore(prop, need) - if (score === null || score < 25) continue - - const locS = locationScore(prop.location.city, need.preferredLocations ?? []) - const isGoodLoc = locS >= 0.9 - - const match: Match = { - id: crypto.randomUUID(), - propertyId: prop.id, - needId: need.id, - resultId: prop.id, - resultType: prop.resultType ?? 'VERIFIED_PORTFOLIO', - matchScore: score, - matchStrength: strengthFromScore(score) as typeof MatchStrength[keyof typeof MatchStrength], - status: score >= 75 ? MatchStatus.PENDING_REVIEW : MatchStatus.PENDING_REVIEW, - scoreBreakdown: { - hardMatchScore: score + 5, - softFactorScore: score - 5, - confidenceModifier: isGoodLoc ? 0.96 : 0.82, - dataQualityModifier: 0.92, - totalScore: score, - }, - positiveFactors: isGoodLoc - ? [{ criterion: 'Standort', weight: 0.25, score: 92, contribution: 23, explanation: `${prop.location.city} – bevorzugter Standort` }] - : [{ criterion: 'Fläche', weight: 0.25, score: 70, contribution: 17.5, explanation: `${prop.areaSqm} m² verfügbar` }], - negativeFactors: !isGoodLoc - ? [{ criterion: 'Standort', weight: 0.25, score: 35, contribution: 8.75, explanation: `${prop.location.city} liegt außerhalb der bevorzugten Region` }] - : [], - tradeoffs: !isGoodLoc - ? [{ criterion: 'Standort', concern: `${prop.location.city} ist nicht im Präferenzgebiet`, severity: 'MEDIUM' as const }] - : [], - explainabilitySummary: isGoodLoc - ? `${prop.location.city} trifft den Standortwunsch. Objekt entspricht den Kernkriterien.` - : `Abweichender Standort (${prop.location.city}). Nur bei Engpass im Zielgebiet empfohlen.`, - confidenceLevel: isGoodLoc ? 0.88 : 0.60, - riskLevel: isGoodLoc ? RiskLevel.LOW : RiskLevel.MEDIUM, - uncertaintyIndicators: isGoodLoc ? [] : ['Standort außerhalb Präferenz'], - organizationId: 'org-wincasa', - createdAt: now, - updatedAt: now, - } - - matchStore.push(match) - } -} - -// ── Provider ─────────────────────────────────────────────────────────────────── - -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) - generateSyntheticMatches(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/.claude/worktrees/agent-a82a3716/src/provider/MockupPropertyProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/MockupPropertyProvider.ts deleted file mode 100644 index b3af131..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/MockupPropertyProvider.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { IPropertyProvider, PropertyFilters } from './IPropertyProvider' -import type { Property, CreatePropertyInput, UpdatePropertyInput } from '../domain/property' -import { mockProperties } from '../mock-data/properties' - -export const propertyStore: Property[] = [...mockProperties] -const store = propertyStore - -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/.claude/worktrees/agent-a82a3716/src/provider/MockupReviewProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/MockupReviewProvider.ts deleted file mode 100644 index ba4a897..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/MockupReviewProvider.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { IReviewProvider, ReviewFilters } from './IReviewProvider' -import type { ReviewTask, ReviewNote } from '../domain/review' -import { mockReviewQueue } from '../mock-data/reviewQueue' -import { mockDelay } from '../lib/mockUtils' - -const store: ReviewTask[] = [...mockReviewQueue] - -const priorityOrder: Record = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 } - -function makeNote(content: string, createdBy: string): ReviewNote { - return { - id: `note-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, - content, - createdBy, - createdAt: new Date().toISOString(), - } -} - -export const MockupReviewProvider: IReviewProvider = { - async getQueue(filters?: ReviewFilters) { - await mockDelay() - let results = [...store] - if (filters?.priority) results = results.filter(r => r.priority === filters.priority) - if (filters?.status) results = results.filter(r => r.status === filters.status) - if (filters?.entityType) results = results.filter(r => r.entityType === filters.entityType) - if (filters?.assignedTo) results = results.filter(r => r.assignedTo === filters.assignedTo) - if (filters?.organizationId) results = results.filter(r => r.relatedOrganizationId === filters.organizationId) - return results.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9)) - }, - - async getById(id) { - await mockDelay() - return store.find(r => r.id === id) ?? null - }, - - async updateStatus(id, status, userId, note?) { - await mockDelay() - const idx = store.findIndex(r => r.id === id) - const notes = [...store[idx].reviewNotes] - if (note) notes.push(makeNote(note, userId)) - store[idx] = { ...store[idx], status, reviewNotes: notes, updatedAt: new Date().toISOString() } - return store[idx] - }, - - async addNote(id, note) { - await mockDelay() - const idx = store.findIndex(r => r.id === id) - const newNote: ReviewNote = { id: `note-${Date.now()}`, ...note } - store[idx] = { - ...store[idx], - reviewNotes: [...store[idx].reviewNotes, newNote], - updatedAt: new Date().toISOString(), - } - return store[idx] - }, - - async assign(id, assignTo) { - await mockDelay() - const idx = store.findIndex(r => r.id === id) - store[idx] = { - ...store[idx], - assignedTo: assignTo, - status: store[idx].status === 'PENDING' ? 'IN_REVIEW' : store[idx].status, - updatedAt: new Date().toISOString(), - } - return store[idx] - }, - - async approve(id, reviewedBy, notes?) { - return this.updateStatus(id, 'APPROVED', reviewedBy, notes) - }, - - async reject(id, reviewedBy, notes?) { - return this.updateStatus(id, 'REJECTED', reviewedBy, notes) - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/MockupShortlistProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/MockupShortlistProvider.ts deleted file mode 100644 index c948dfd..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/MockupShortlistProvider.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { IShortlistProvider, ShortlistFilters } from './IShortlistProvider' -import type { Shortlist, CreateShortlistInput, UpdateShortlistInput, ShortlistItemInput } from '../domain/shortlist' -import { mockShortlists } from '../mock-data/shortlists' -import { mockDelay } from '../lib/mockUtils' - -const store: Shortlist[] = [...mockShortlists] - -export const MockupShortlistProvider: IShortlistProvider = { - async getAll(filters?: ShortlistFilters) { - await mockDelay() - let results = [...store] - if (filters?.needId) results = results.filter(s => s.needId === filters.needId) - if (filters?.status) results = results.filter(s => s.status === filters.status) - if (filters?.createdBy) results = results.filter(s => s.createdBy === filters.createdBy) - if (filters?.organizationId) results = results.filter(s => s.organizationId === filters.organizationId) - return results - }, - async getById(id) { - await mockDelay() - return store.find(s => s.id === id) ?? null - }, - async create(data: CreateShortlistInput) { - await mockDelay() - const next: Shortlist = { - id: crypto.randomUUID(), - ...data, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - } - store.push(next) - return next - }, - async update(id, data: UpdateShortlistInput) { - await mockDelay() - const idx = store.findIndex(s => s.id === id) - store[idx] = { ...store[idx], ...data, updatedAt: new Date().toISOString() } - return store[idx] - }, - async addItem(id, item: ShortlistItemInput) { - await mockDelay() - const idx = store.findIndex(s => s.id === id) - const alreadyAdded = store[idx].items.some(i => i.resultId === item.resultId) - if (!alreadyAdded) { - store[idx] = { - ...store[idx], - items: [...store[idx].items, { ...item, addedAt: new Date().toISOString() }], - updatedAt: new Date().toISOString(), - } - } - return store[idx] - }, - async removeItem(id, resultId) { - await mockDelay() - const idx = store.findIndex(s => s.id === id) - store[idx] = { - ...store[idx], - items: store[idx].items.filter(i => i.resultId !== resultId), - updatedAt: new Date().toISOString(), - } - return store[idx] - }, - async remove(id) { - await mockDelay() - const idx = store.findIndex(s => s.id === id) - store.splice(idx, 1) - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/provider/MockupSignalPipelineProvider.ts b/.claude/worktrees/agent-a82a3716/src/provider/MockupSignalPipelineProvider.ts deleted file mode 100644 index ec45a71..0000000 --- a/.claude/worktrees/agent-a82a3716/src/provider/MockupSignalPipelineProvider.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { ISignalPipelineProvider } from './ISignalPipelineProvider' -import type { PipelineState } from '../domain/signalPipeline' -import { GateStatus, PipelineStage } from '../domain/signalPipeline' -import type { GateType } from '../domain/signalPipeline' -import { MOCK_PIPELINE_STATES, MOCK_AUDIT_TRAILS } from '../mock-data/signalPipelines' - -let states: PipelineState[] = [...MOCK_PIPELINE_STATES] - -export const MockupSignalPipelineProvider: ISignalPipelineProvider = { - async getPipelineState(signalId) { - return states.find(s => s.signalId === signalId) ?? null - }, - async evaluateGate(signalId, gateType) { - const idx = states.findIndex(s => s.signalId === signalId) - if (idx === -1) throw new Error(`Pipeline state for ${signalId} not found`) - // Demo: mark gate as re-evaluated (no real logic change) - const updated: PipelineState = { - ...states[idx], - gates: { - ...states[idx].gates, - [gateType]: { - ...states[idx].gates[gateType as GateType], - evaluatedAt: new Date().toISOString(), - }, - }, - } - states[idx] = updated - return updated - }, - async getAuditTrail(signalId) { - return MOCK_AUDIT_TRAILS.filter(e => e.signalId === signalId) - .sort((a, b) => b.timestamp.localeCompare(a.timestamp)) - }, - async publishToFutureAvailability(signalId) { - const idx = states.findIndex(s => s.signalId === signalId) - if (idx === -1) throw new Error(`Pipeline state for ${signalId} not found`) - const now = new Date().toISOString() - states[idx] = { - ...states[idx], - publishedToFutureAvailability: true, - publishedAt: now, - currentStage: PipelineStage.STAGE_6_MATCHABLE_RESULT, - gates: { - ...states[idx].gates, - FEED_ELIGIBILITY_GATE: { - ...states[idx].gates.FEED_ELIGIBILITY_GATE, - status: GateStatus.PASSED, - reason: 'Signal manuell in Future Availability publiziert.', - evaluatedAt: now, - checks: [{ label: 'Manuell publiziert', passed: true, value: 'Ja' }], - }, - }, - overallEligible: true, - } - return states[idx] - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/aiAssistantService.ts b/.claude/worktrees/agent-a82a3716/src/services/aiAssistantService.ts deleted file mode 100644 index dea3936..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/aiAssistantService.ts +++ /dev/null @@ -1,409 +0,0 @@ -import type { AssistantContext, AssistantMessage, SuggestedQuestion, AssistantAction } from '../domain/assistant' - -const delay = (ms: number) => new Promise(r => setTimeout(r, ms)) - -// ── Page-type resolution ─────────────────────────────────────────────────────── - -function pageType(route: string): string { - if (/\/supply\/properties\/.+/.test(route)) return 'property-detail' - if (route.includes('/supply/match-center')) return 'match-center' - if (route.includes('/supply/data-quality')) return 'data-quality' - if (route.includes('/supply/future-availability')) return 'future-availability' - if (route.includes('/supply/dashboard')) return 'supply-dashboard' - if (route.includes('/demand/results')) return 'demand-results' - if (route.includes('/demand/compare')) return 'compare' - if (route.includes('/demand/ai-search')) return 'ai-search' - if (route.includes('/ops/review-queue')) return 'review-queue' - if (route.includes('/ops/ai-monitoring')) return 'ai-monitoring' - return 'general' -} - -// ── Suggestions per page type ───────────────────────────────────────────────── - -const SUGGESTIONS: Record = { - 'property-detail': [ - { id: 'pd1', question: 'Warum passt dieses Objekt nicht gut zu aktuellen Gesuchen?', category: 'Match' }, - { id: 'pd2', question: 'Welche Daten sollte ich zuerst verbessern?', category: 'Datenqualität' }, - { id: 'pd3', question: 'Welche Suchprofile passen am besten zu diesem Objekt?', category: 'Match' }, - { id: 'pd4', question: 'Wie gross ist das Risiko, dieses Objekt nicht zu vermieten?', category: 'Risiko' }, - ], - 'match-center': [ - { id: 'mc1', question: 'Welcher eingehende Bedarf hat die höchste Priorität?', category: 'Priorisierung' }, - { id: 'mc2', question: 'Warum hat dieser Match einen niedrigen Score?', category: 'Match' }, - { id: 'mc3', question: 'Soll ich den Kontakt für diesen Match freigeben?', category: 'Aktion' }, - ], - 'demand-results': [ - { id: 'dr1', question: 'Warum ist dieses Ergebnis an erster Stelle?', category: 'Ranking' }, - { id: 'dr2', question: 'Was sind die grössten Kompromisse bei diesem Match?', category: 'Tradeoffs' }, - { id: 'dr3', question: 'Sollte ich alternative Standorte in Betracht ziehen?', category: 'Strategie' }, - { id: 'dr4', question: 'Welche Hardkriterien werden am häufigsten nicht erfüllt?', category: 'Analyse' }, - ], - 'compare': [ - { id: 'co1', question: 'Welche Option ist strategisch am besten?', category: 'Empfehlung' }, - { id: 'co2', question: 'Welche Option hat das höchste Risiko?', category: 'Risiko' }, - { id: 'co3', question: 'Welche Option ist am kostengünstigsten?', category: 'Kosten' }, - ], - 'data-quality': [ - { id: 'dq1', question: 'Was sollte ich zuerst beheben?', category: 'Priorität' }, - { id: 'dq2', question: 'Welche fehlenden Felder haben den grössten Einfluss auf Matches?', category: 'Impact' }, - { id: 'dq3', question: 'Wie verbessere ich den Datenqualitäts-Score schnell?', category: 'Optimierung' }, - ], - 'future-availability': [ - { id: 'fa1', question: 'Warum ist dieses Signal probabilistisch und nicht bestätigt?', category: 'Erklärung' }, - { id: 'fa2', question: 'Welche Belege unterstützen dieses Signal?', category: 'Evidenz' }, - { id: 'fa3', question: 'Was muss vor der Freigabe an Demand-Nutzer geprüft werden?', category: 'Review' }, - { id: 'fa4', question: 'Wie hoch ist die Konfidenz dieses Signals?', category: 'Konfidenz' }, - ], - 'review-queue': [ - { id: 'rq1', question: 'Welche Review-Aufgabe sollte ich zuerst bearbeiten?', category: 'Priorisierung' }, - { id: 'rq2', question: 'Was sind die Kriterien für eine Genehmigung?', category: 'Prozess' }, - { id: 'rq3', question: 'Wann sollte ich eine Aufgabe eskalieren?', category: 'Eskalation' }, - ], - 'ai-monitoring': [ - { id: 'am1', question: 'Welche fehlgeschlagenen Outputs haben die höchste Priorität?', category: 'Fehler' }, - { id: 'am2', question: 'Was bedeutet ein Schema-Validierungsfehler?', category: 'Fehleranalyse' }, - { id: 'am3', question: 'Welche AI-Outputs brauchen eine manuelle Review?', category: 'Review' }, - ], - 'general': [ - { id: 'g1', question: 'Wie kann ich meine Daten für bessere Matches vorbereiten?', category: 'Optimierung' }, - { id: 'g2', question: 'Was sind die wichtigsten KPIs in dieser Ansicht?', category: 'Überblick' }, - { id: 'g3', question: 'Welche nächste Aktion empfiehlst du?', category: 'Aktion' }, - ], -} - -// ── Answer templates ─────────────────────────────────────────────────────────── - -type AnswerPayload = { - content: string - confidence: number - sources: string[] - actions?: AssistantAction[] -} - -type Template = { - keywords: string[] - generate: (ctx: AssistantContext) => AnswerPayload -} - -const entityRef = (ctx: AssistantContext) => - ctx.selectedEntityId ? ` (${ctx.selectedEntityId})` : '' - -const missingFields = (ctx: AssistantContext) => - ctx.visibleMissingData?.slice(0, 3).join(', ') ?? 'Mietpreis/m², Verfügbarkeit' - -const scoreVal = (ctx: AssistantContext, key: string, fallback = 72) => - ctx.visibleScores?.[key] ?? fallback - -const TEMPLATES: Record = { - 'property-detail': [ - { - keywords: ['passt', 'match', 'score', 'niedrig'], - generate: (_ctx) => ({ - content: `Das Objekt${entityRef(_ctx)} erreicht einen Datenqualitätsscore von ${scoreVal(_ctx, 'quality')}%. Damit liegt es unter dem empfohlenen Schwellenwert von 70%, der für präzises Matching erforderlich ist.\n\nDie häufigsten Faktoren, die Matches verhindern:\n• Fehlende oder veraltete Felder (${missingFields(_ctx)})\n• Unklare Verfügbarkeitsangaben – kritisch für zeitbasierte Gesuche\n• Fehlende Zertifizierungen, wenn Demand-Profile spezifische Anforderungen haben\n\nEmpfehlung: Qualitätsfelder priorisieren, um den Score auf ≥75% zu bringen und die Sichtbarkeit in der Trefferquote zu erhöhen.`, - confidence: 0.86, - sources: ['Datenqualität', 'Match-Score-Berechnung'], - actions: [ - { id: 'a1', label: 'Zur Datenpflege', description: 'Datenqualität dieses Objekts verbessern', actionType: 'NAVIGATE', payload: { path: '/supply/data-quality' } }, - ], - }), - }, - { - keywords: ['verbessern', 'zuerst', 'priorität', 'beheben', 'felder'], - generate: (_ctx) => ({ - content: `Für Objekt${entityRef(_ctx)} empfehle ich folgende Reihenfolge:\n\n**1. ${_ctx.visibleMissingData?.[0] ?? 'Mietpreis/m²'}** (kritisch)\nDirekte Auswirkung auf 60–70% aller Bedarfsanfragen. Ohne Preisinformation kein Matching möglich.\n\n**2. ${_ctx.visibleMissingData?.[1] ?? 'Verfügbarkeitsdatum'}** (hoch)\nZeitbasierte Gesuche schliessen Objekte ohne klares Datum aus.\n\n**3. ${_ctx.visibleMissingData?.[2] ?? 'Fläche m²'}** (mittel)\nBestimmt, ob Flächenkriterien erfüllt werden.\n\nNach diesen drei Feldern sollte der Qualitätsscore um ~15–20 Punkte steigen.`, - confidence: 0.91, - sources: ['Datenqualität', 'Feldgewichtung'], - actions: [ - { id: 'a2', label: 'Felder aktualisieren', description: 'Objekt-Detailansicht öffnen und Felder bearbeiten', actionType: 'NAVIGATE', payload: { path: '/supply/properties' } }, - ], - }), - }, - { - keywords: ['suchprofile', 'gesuche', 'demand', 'passend', 'passen'], - generate: (_ctx) => ({ - content: `Basierend auf dem aktuellen Objekt${entityRef(_ctx)} würden vor allem Profile mit folgenden Eigenschaften passen:\n\n• **Büro / Open Space** – sofern Grundriss offen oder teilbar\n• **Mittleres Budget** (CHF 8'000–14'000/Mt) – entspricht typischer Preisrange\n• **Kurzfristige Verfügbarkeit** (≤3 Monate) – hohe Nachfrage in diesem Segment\n\nFür genaue Profilvorschläge: Den Match-Center öffnen und die Trefferrate mit aktuellen Gesuchen prüfen.`, - confidence: 0.78, - sources: ['Match-Center', 'Demand-Profile-Analyse'], - actions: [ - { id: 'a3', label: 'Match-Center öffnen', description: 'Eingehende Bedarfe für dieses Objekt anzeigen', actionType: 'NAVIGATE', payload: { path: '/supply/match-center' } }, - ], - }), - }, - { - keywords: ['risiko', 'risk', 'leerstand', 'vermieten'], - generate: (_ctx) => ({ - content: `Das Leerstandsrisiko für Objekt${entityRef(_ctx)} hängt von drei Faktoren ab:\n\n• **Datenqualität** (${scoreVal(_ctx, 'quality')}%) – Niedrige Qualität reduziert Sichtbarkeit in Suchergebnissen\n• **Marktlage** – Aktuelle Signale deuten auf moderate Nachfrage in diesem Segment hin\n• **Preispositionierung** – Ohne Marktpreisvergleich keine verlässliche Einschätzung möglich\n\n**Hinweis:** Diese Einschätzung basiert auf verfügbaren Metadaten. Für eine fundierte Leerstandsprognose wird eine vollständige Datenbasis empfohlen.`, - confidence: 0.71, - sources: ['Datenqualität', 'Marktindikatoren'], - }), - }, - ], - - 'demand-results': [ - { - keywords: ['ersten', 'erst', 'ranking', 'warum', 'platz'], - generate: (_ctx) => ({ - content: `Das erstplatzierte Ergebnis${entityRef(_ctx)} erreicht diesen Rang, weil es die meisten Hardkriterien vollständig erfüllt. Im Scoring-Modell zählen Hardkriterien mit 60% Gewichtung – ein Objekt mit 5/5 Hardkriterien übertrifft alle Objekte mit auch nur einem unerfüllten Kriterium.\n\nZusätzlich fliessen Softfaktoren (40%) ein: Standortqualität, Verfügbarkeitsübereinstimmung und Ausbaustandard.\n\nFür Details zur Begründung: "Match-Erklärung" in der Detailansicht öffnen.`, - confidence: 0.89, - sources: ['Match-Score', 'Scoring-Modell'], - }), - }, - { - keywords: ['kompromiss', 'trade', 'nachteil', 'tradeoff', 'opfer'], - generate: (_ctx) => ({ - content: `Die grössten Kompromisse bei diesem Match:\n\n• **Preis vs. Fläche** – Das Objekt liegt ggf. über Budget, bietet aber mehr Fläche als Minimum\n• **Lage vs. Ausbaustandard** – Zentralere Lage geht oft mit höherem Mietpreis einher\n• **Verfügbarkeit** – Falls Objekt erst in 4+ Monaten frei wird, widerspricht das kurzfristigen Bedarfen\n\n**Empfehlung:** Tradeoffs mit dem Suchenden diskutieren – was ist verhandelbar, was ist ein Ausschlusskriterium?`, - confidence: 0.83, - sources: ['Match-Score', 'Hardkriterien-Analyse'], - actions: [ - { id: 'a4', label: 'Vergleichsansicht öffnen', description: 'Ergebnis mit anderen Matches vergleichen', actionType: 'NAVIGATE', payload: { path: '/demand/compare' } }, - ], - }), - }, - { - keywords: ['alternative', 'standort', 'lage', 'andere'], - generate: (_ctx) => ({ - content: `Alternative Standorte lohnen sich zu prüfen, wenn:\n\n• Die Top-Ergebnisse alle im selben Preissegment liegen und Budget ein Engpass ist\n• Die Anforderungen an Lage verhandelbar sind (z.B. Zürich 1–4 statt nur 1)\n• Suchprofile mit erweiterter Standorttoleranz signifikant bessere Treffer zeigen\n\n**Konkret:** Im AI-Suche-Formular die Standortangabe auf Stadtkreis oder Kanton ausweiten und neu suchen. Dies kann die Trefferanzahl um 30–60% erhöhen.`, - confidence: 0.80, - sources: ['Suchanfrage-Analyse', 'Standort-Scoring'], - actions: [ - { id: 'a5', label: 'Suche anpassen', description: 'Zurück zur Flächensuche mit erweiterter Standortauswahl', actionType: 'NAVIGATE', payload: { path: '/demand/ai-search' } }, - ], - }), - }, - { - keywords: ['hardkriterien', 'kriterien', 'nicht erfüllt', 'ausschlusskriterium'], - generate: (_ctx) => ({ - content: `Häufig nicht erfüllte Hardkriterien in den aktuellen Ergebnissen:\n\n• **Flächengrösse** – Viele Objekte liegen 10–20% unter dem Mindestwert\n• **Verfügbarkeitsdatum** – Diskrepanz zwischen gewünschtem Einzugsdatum und tatsächlicher Verfügbarkeit\n• **Parkplatzkontingent** – Wenige Objekte bieten die geforderte Anzahl Stellplätze\n\nHinweis: Hardkriterien sind binär – ein nicht erfülltes Kriterium schiesst ein Objekt vollständig aus dem Ranking aus, unabhängig von anderen Stärken.`, - confidence: 0.88, - sources: ['Matching-Engine', 'Kriterien-Gewichtung'], - }), - }, - ], - - 'compare': [ - { - keywords: ['strategisch', 'best', 'empfehlung', 'wählen'], - generate: (_ctx) => ({ - content: `Für eine strategische Empfehlung werden folgende Dimensionen gewichtet:\n\n• **Match-Score** – Wie gut erfüllt das Objekt das Suchprofil?\n• **Datenqualität** – Je vollständiger, desto verlässlicher die Einschätzung\n• **Zeitliche Verfügbarkeit** – Passt der Einzugstermin zur Planung?\n• **Preis-Leistung** – Mietpreis im Verhältnis zu Fläche und Ausstattung\n\n**Hinweis:** Die finale Entscheidung muss durch den Nutzer getroffen werden. Der Assistant kann Faktoren gewichten, aber keine verbindliche Empfehlung ohne vollständige Datenbasis abgeben.`, - confidence: 0.77, - sources: ['Vergleichsansicht', 'Match-Scores'], - }), - }, - { - keywords: ['risiko', 'höchste', 'gefährlich', 'risikoreiche'], - generate: (_ctx) => ({ - content: `Risikoindikatoren im Vergleich:\n\n• **Niedrige Datenqualität** (<65%) = höheres Informationsrisiko – Angaben nicht verlässlich verifiziert\n• **Niedrige Konfidenz** (<60%) = Scoring-Unsicherheit – Match könnte sich bei mehr Daten verschlechtern\n• **Fehlende Verfügbarkeitsangabe** = Planungsrisiko – keine verbindliche Zusage möglich\n\nDas Objekt mit dem niedrigsten Konfidenz-Score trägt das höchste strukturelle Risiko, weil die Basis für den Match-Score unvollständig ist.`, - confidence: 0.84, - sources: ['Konfidenz-Scores', 'Datenqualität'], - }), - }, - { - keywords: ['kosten', 'günstig', 'preis', 'effektiv', 'billiger'], - generate: (_ctx) => ({ - content: `Kostenbewertung im Vergleich:\n\nDie reine Mietkosten-Betrachtung reicht nicht aus. Relevant ist der **Preis pro m²** im Verhältnis zu:\n• Ausstattungsstandard und Renovierungszustand\n• Nebenkosten und Betriebskosten\n• Lagequalität (ÖPNV, Infrastruktur)\n\nEin günstigeres Objekt mit hohem Renovierungsbedarf kann mittelfristig teurer werden als ein teureres, bezugsbereites Objekt.\n\n**Tipp:** Mietpreis/m² in der Vergleichstabelle nebeneinander stellen und Gesamtkosten über Mietdauer schätzen.`, - confidence: 0.79, - sources: ['Preisangaben', 'Kostenvergleich'], - }), - }, - ], - - 'data-quality': [ - { - keywords: ['zuerst', 'priorität', 'erst', 'beheben', 'anfangen'], - generate: (_ctx) => ({ - content: `**Empfohlene Prioritäten für sofortigen Impact:**\n\n1. **${_ctx.visibleMissingData?.[0] ?? 'Mietpreis/m²'}** — Kritisch\nOhne Preisinformation werden Objekte aus preissensitiven Suchanfragen ausgeschlossen.\n\n2. **${_ctx.visibleMissingData?.[1] ?? 'Verfügbarkeitsdatum'}** — Hoch\nZeitbasierte Matching-Logik erfordert ein konkretes Datum.\n\n3. **${_ctx.visibleMissingData?.[2] ?? 'Adresse / Koordinaten'}** — Mittel\nSuchradius-Filter benötigen geografische Verortung.\n\nNach diesen drei Feldern ist ein Qualitätsscore von ≥75% erreichbar – der Schwellenwert für volle Matching-Sichtbarkeit.`, - confidence: 0.93, - sources: ['Feldgewichtung', 'Matching-Regeln'], - actions: [ - { id: 'a6', label: 'Objekt bearbeiten', description: 'Kritische Felder in der Objektansicht aktualisieren', actionType: 'NAVIGATE', payload: { path: '/supply/properties' } }, - ], - }), - }, - { - keywords: ['fehlende', 'felder', 'impact', 'einfluss', 'auswirkung'], - generate: (_ctx) => ({ - content: `Einfluss fehlender Felder auf Match-Trefferquote:\n\n| Feld | Ausschlussquote |\n|------|----------------|\n| Mietpreis | ~65% aller Gesuche |\n| Fläche m² | ~80% aller Gesuche |\n| Verfügbarkeit | ~50% zeitkritischer Gesuche |\n| Zertifizierungen | ~20–30% spezifischer Gesuche |\n\nDie Fläche hat die grösste Ausschlussquote, da sie das primäre Hardkriterium für nahezu alle Suchprofile ist.`, - confidence: 0.90, - sources: ['Matching-Engine', 'Statistik-Analyse'], - }), - }, - { - keywords: ['score', 'verbessern', 'erhöhen', 'schnell', 'steigern'], - generate: (_ctx) => ({ - content: `Schnellste Wege zur Score-Verbesserung:\n\n• **Vollständigkeits-Boost** (+15–20 Punkte): Die 3 wichtigsten kritischen Felder befüllen\n• **Aktualitäts-Boost** (+5–10 Punkte): Letzte Aktualisierung auf heute setzen\n• **Verifikations-Boost** (+10 Punkte): Quellenangaben zu Preisen und Verfügbarkeit hinzufügen\n\nHinweis: Der Qualitätsscore wird bei jeder Änderung neu berechnet. Kein Warten nötig.`, - confidence: 0.87, - sources: ['Score-Berechnung', 'Feldgewichtung'], - }), - }, - ], - - 'future-availability': [ - { - keywords: ['probabilistisch', 'bestätigt', 'nicht bestätigt', 'warum', 'unbestätigt'], - generate: (_ctx) => ({ - content: `**Warum ist das Signal probabilistisch?**\n\nDieses Signal basiert auf indirekten Datenquellen (Baugesuche, Stellenausschreibungen, Pressemitteilungen) – nicht auf einer direkten Bestätigung durch den Vermieter oder Eigentümer.\n\nDie Verfügbarkeit ist eine **Wahrscheinlichkeitsaussage**, keine Tatsache. Das bedeutet:\n• Die Fläche ist möglicherweise noch nicht auf dem Markt\n• Die Zeitangabe kann sich verschieben\n• Eine alternative Nutzung ist nicht ausgeschlossen\n\n⚠️ Demand-Nutzern gegenüber darf dieses Signal nie als bestätigte Verfügbarkeit kommuniziert werden.`, - confidence: 0.95, - sources: ['Signal-Typ', 'Quellenklassifikation'], - }), - }, - { - keywords: ['belege', 'evidence', 'beweise', 'unterstützen', 'daten'], - generate: (_ctx) => ({ - content: `Belege für dieses Signal werden aus folgenden Quellen abgeleitet:\n\n• **Quellentyp** des Signals (z.B. Baugesuch, Jobausschreibung, Pressemitteilung)\n• **Erscheinungsdatum** der Quelle\n• **Konfidenzwert** basierend auf Quellenzuverlässigkeit und Korroborierung\n\nFür spezifische Belege: Signal-Detailansicht öffnen → Abschnitt "Evidenz".\n\nHinweis: Ein einzelner Beleg ohne Korroborierung senkt den Konfidenzwert. Mehrere unabhängige Quellen erhöhen ihn.`, - confidence: 0.88, - sources: ['Evidenz-Modul', 'Quellen-Klassifikation'], - actions: [ - { id: 'a7', label: 'Signal-Details öffnen', description: 'Evidenz-Abschnitt für dieses Signal anzeigen', actionType: 'NAVIGATE', payload: { path: '/supply/future-availability' } }, - ], - }), - }, - { - keywords: ['review', 'prüfen', 'freigabe', 'zeigen', 'demand'], - generate: (_ctx) => ({ - content: `**Vor der Freigabe an Demand-Nutzer empfehle ich:**\n\n1. **Konfidenz prüfen** – Signal sollte ≥60% haben, sonst nur intern sichtbar lassen\n2. **Sensitivitätsstufe prüfen** – CONFIDENTIAL-Signale nie extern zeigen\n3. **Review-Status** – Signal muss mindestens IN_REVIEW-Status haben\n4. **Haftungshinweis** – Disclaimer muss für Demand-Nutzer sichtbar sein\n\nFür die Freigabe: "Zur Prüfung senden" in der Signal-Detailansicht klicken.`, - confidence: 0.92, - sources: ['Review-Workflow', 'Disclosure-Regeln'], - actions: [ - { id: 'a8', label: 'Review Queue öffnen', description: 'Signal zur manuellen Prüfung übergeben', actionType: 'OPEN_REVIEW' }, - ], - }), - }, - { - keywords: ['konfidenz', 'wahrscheinlichkeit', 'probability', 'genau'], - generate: (_ctx) => ({ - content: `Der Konfidenzwert für dieses Signal setzt sich zusammen aus:\n\n• **Quellenqualität** (0–40%): Offizielle Quellen (Baugesuche, Amtsblatt) zählen höher als Pressemitteilungen\n• **Zeitnähe** (0–30%): Ältere Quellen werden abgewertet\n• **Korroborierung** (0–30%): Mehrere unabhängige Quellen erhöhen den Wert\n\nEin Wert unter 50% deutet auf unzuverlässige oder einzelne Quellen hin und sollte als "beobachtenswert, nicht aktionierbar" behandelt werden.`, - confidence: 0.85, - sources: ['Konfidenz-Berechnung', 'Quellengewichtung'], - }), - }, - ], - - 'review-queue': [ - { - keywords: ['zuerst', 'priorität', 'dringend', 'welche'], - generate: (_ctx) => ({ - content: `**Priorisierung der Review Queue:**\n\nEmpfohlene Reihenfolge nach Dringlichkeit:\n\n1. **CRITICAL + ESCALATED** – Sofortiger Handlungsbedarf, meist rechtliche oder Compliance-Relevanz\n2. **HIGH + PENDING** – Warten auf Entscheidung, können Prozesse blockieren\n3. **Fälligkeitsdatum überschritten** – Unabhängig von Priorität\n4. **MEDIUM + IN_REVIEW** – Bereits in Bearbeitung, weiterführen\n\nAufgaben ohne Fälligkeitsdatum und mit LOW-Priorität können gebündelt am Ende bearbeitet werden.`, - confidence: 0.90, - sources: ['Review-Queue-Regeln', 'Prioritäts-Framework'], - }), - }, - { - keywords: ['kriterien', 'genehmigen', 'ablehnen', 'genehmigung'], - generate: (_ctx) => ({ - content: `**Entscheidungskriterien:**\n\n✅ **Genehmigen**, wenn:\n• Alle Pflichtfelder vorhanden und plausibel\n• Konfidenz ≥65%\n• Kein offensichtlicher Datenfehler\n• Quellen verifizierbar\n\n❌ **Ablehnen**, wenn:\n• Schema-Validierungsfehler vorliegt\n• Inhalte nachweislich falsch oder irreführend\n• Datenschutz-Bedenken nicht ausgeräumt\n\n⚠️ **Mehr Daten anfordern**, wenn:\n• Wichtige Felder fehlen aber beschaffbar sind\n• Quelle unklar, aber plausibel`, - confidence: 0.93, - sources: ['Governance-Richtlinien', 'Review-Protokoll'], - }), - }, - { - keywords: ['eskalier', 'eskalation', 'wann', 'hochstufen'], - generate: (_ctx) => ({ - content: `**Eskalation ist angemessen wenn:**\n\n• Die Entscheidung Rechtsfolgen hat (Datenschutz, GDPR, Mietrecht)\n• Konflikte zwischen Stakeholdern nicht auf Reviewer-Ebene lösbar sind\n• Der Review-Task eine Geschäftsentscheidung mit hohem Risiko erfordert\n• Zwei Reviewer zu unterschiedlichen Ergebnissen kommen\n\nEskalierte Tasks landen bei der Organisationsleitung. Nutzung sparsam empfohlen – zu viele Eskalationen entwerten das Signal.`, - confidence: 0.88, - sources: ['Eskalations-Framework', 'Governance'], - }), - }, - ], - - 'ai-monitoring': [ - { - keywords: ['fehler', 'fehlgeschlagen', 'priorität', 'wichtig'], - generate: (_ctx) => ({ - content: `**Fehler-Triage in der Reihenfolge:**\n\n1. **SCHEMA_VALIDATION** – Höchste Priorität. Output wurde nicht an die UI geliefert. Nutzer hat möglicherweise unvollständige Informationen erhalten.\n2. **EMPTY_RESPONSE** – Hoch. Funktion hat komplett versagt. Retry empfehlenswert.\n3. **INVALID_JSON** – Mittel. Output war vorhanden, aber nicht verarbeitbar. Recovery oft möglich.\n4. **PROVIDER_TIMEOUT** – Niedrig bis Mittel. Meist temporäres Problem. Retry oder Fallback prüfen.\n\nFür alle Fehler mit FLAGGED-Status: Review-Aufgabe erstellen, um manuellen Check zu dokumentieren.`, - confidence: 0.91, - sources: ['Fehler-Klassifikation', 'AI-Monitoring'], - actions: [ - { id: 'a9', label: 'Fehler filtern', description: 'AI-Monitoring-Tabelle auf Fehler filtern', actionType: 'NAVIGATE', payload: { path: '/ops/ai-monitoring' } }, - ], - }), - }, - { - keywords: ['schema', 'validierung', 'schema-fehler', 'bedeutet'], - generate: (_ctx) => ({ - content: `**Schema-Validierungsfehler erklärt:**\n\nEin Schema-Validierungsfehler bedeutet, dass der AI-Output zwar generiert wurde, aber nicht der erwarteten Datenstruktur entspricht.\n\n**Mögliche Ursachen:**\n• Pflichtfeld fehlt im Output (z.B. 'hardCriteria')\n• Falscher Datentyp (z.B. String statt Number)\n• Prompt-/Schema-Versions-Mismatch\n\n**Konsequenz:** Der Output wurde **nicht** an die UI ausgeliefert – der Nutzer hat kein fehlerhaftes Resultat gesehen.\n\n**Massnahme:** Prompt-Version und Schema-Version prüfen, ggf. Prompt aktualisieren.`, - confidence: 0.94, - sources: ['Schema-Validierung', 'AI-Pipeline'], - }), - }, - { - keywords: ['review', 'manuell', 'prüfung', 'brauchen'], - generate: (_ctx) => ({ - content: `**AI-Outputs, die manuelle Review brauchen:**\n\n• Status **FLAGGED** – wurde automatisch als problematisch markiert\n• Status **UNREVIEWED** + Fehler vorhanden – hohe Priorität\n• Outputs mit **DECISION_BRIEF** oder **MATCH_EXPLANATION** Typ – direkte Auswirkung auf Nutzerentscheidungen\n• Latenz >5s – deutet auf Qualitätsprobleme hin\n\nOutput direkt in der Review Queue anlegen: "Zur Prüfung" Button in der Detail-Ansicht.`, - confidence: 0.89, - sources: ['Review-Regeln', 'AI-Monitoring'], - actions: [ - { id: 'a10', label: 'Review Queue öffnen', description: 'Zur Review Queue navigieren', actionType: 'NAVIGATE', payload: { path: '/ops/review-queue' } }, - ], - }), - }, - ], - - 'general': [ - { - keywords: ['kpi', 'kennzahlen', 'überblick', 'metriken'], - generate: () => ({ - content: `Die wichtigsten KPIs je Workspace:\n\n**Verwaltung (Supply):**\n• Datenqualitäts-Score (Ziel: ≥70%)\n• Match-Rate (Anteil Objekte mit ≥1 aktivem Match)\n\n**Suche (Demand):**\n• Trefferquote (Ergebnisse mit Score ≥70%)\n• Hardkriterien-Erfüllungsrate\n\n**Administration (Ops):**\n• Offene Review-Tasks\n• AI-Fehlerrate\n• Genehmigungsrate`, - confidence: 0.82, - sources: ['Dashboard', 'Monitoring'], - }), - }, - { - keywords: ['nächste', 'aktion', 'empfehlung', 'was tun', 'handlung'], - generate: (_ctx) => ({ - content: `Empfohlene nächste Aktionen basierend auf dem aktuellen Workspace:\n\n• **Datenpflege-Backlog abarbeiten** – Objekte unter 65% Qualitätsscore priorisieren\n• **Review Queue prüfen** – Offene CRITICAL-Tasks zuerst\n• **AI-Fehler quittieren** – FLAGGED-Outputs in AI-Monitoring markieren\n\nDer Assistant kann konkretere Empfehlungen geben, wenn eine spezifische Seite (Objekt, Match, Signal) geöffnet ist.`, - confidence: 0.75, - sources: ['Kontextanalyse'], - }), - }, - { - keywords: ['daten', 'vorbereiten', 'matches', 'bessere'], - generate: () => ({ - content: `**Daten für bessere Matches vorbereiten:**\n\n1. **Vollständigkeit** – Alle Pflichtfelder (Fläche, Preis, Verfügbarkeit, Adresse) befüllen\n2. **Aktualität** – Veraltete Angaben (>6 Monate) aktualisieren\n3. **Präzision** – Exakte m²-Angaben statt Schätzwerte\n4. **Kontext** – Beschreibung von Ausstattung und Besonderheiten hilft der semantischen Suche\n\nJedes komplett befüllte und aktuelle Objekt erhöht die Match-Sichtbarkeit signifikant.`, - confidence: 0.88, - sources: ['Matching-Regeln', 'Best-Practices'], - }), - }, - ], -} - -// ── Template matching ───────────────────────────────────────────────────────── - -function findTemplate(question: string, pageCtx: string): Template | null { - const q = question.toLowerCase() - const bucket = TEMPLATES[pageCtx] ?? TEMPLATES['general'] ?? [] - for (const t of bucket) { - if (t.keywords.some(kw => q.includes(kw))) return t - } - return bucket[0] ?? TEMPLATES['general']?.[0] ?? null -} - -// ── Public service API ──────────────────────────────────────────────────────── - -export const aiAssistantService = { - async getSuggestions(context: AssistantContext): Promise { - await delay(200) - const page = pageType(context.currentRoute) - return (SUGGESTIONS[page] ?? SUGGESTIONS['general']).slice(0, 4) - }, - - async answerQuestion(context: AssistantContext, question: string): Promise> { - await delay(700 + Math.random() * 700) - const page = pageType(context.currentRoute) - const template = findTemplate(question, page) ?? findTemplate(question, 'general') - - if (!template) { - return { - content: 'Zu dieser Frage liegen derzeit keine ausreichenden Kontextdaten vor. Bitte öffnen Sie eine spezifische Objekt- oder Match-Ansicht und stellen Sie die Frage erneut.', - confidence: 0.5, - sources: [], - } - } - - return template.generate(context) - }, - - async createActionFromAnswer(_action: import('../domain/assistant').AssistantAction): Promise<{ success: boolean }> { - await delay(100) - return { success: true } - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/aiMonitoringService.ts b/.claude/worktrees/agent-a82a3716/src/services/aiMonitoringService.ts deleted file mode 100644 index 3977961..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/aiMonitoringService.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MockupAIMonitoringProvider } from '../provider/MockupAIMonitoringProvider' -import type { AIMonitoringFilters } from '../provider/IAIMonitoringProvider' -import type { AIOutput } from '../domain/aiOutput' -import type { ReviewStatus } from '../domain/enums' -import type { ListResponse, ItemResponse } from './types' - -const provider = MockupAIMonitoringProvider - -export const aiMonitoringService = { - async getOutputs(filters?: AIMonitoringFilters): Promise> { - const data = await provider.getOutputs(filters) - return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } - }, - - async getOutput(id: string): Promise> { - const data = await provider.getOutput(id) - return { data } - }, - - async updateReviewStatus(id: string, status: ReviewStatus): Promise> { - const data = await provider.updateReviewStatus(id, status) - return { data } - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/aiService.ts b/.claude/worktrees/agent-a82a3716/src/services/aiService.ts deleted file mode 100644 index 8fd04a5..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/aiService.ts +++ /dev/null @@ -1,424 +0,0 @@ -import type { ItemResponse, ServiceError } from './types' -import { ServiceErrorCode } from './types' -import type { CreateNeedInput } from '../domain/need' -import type { AssetType } from '../domain/enums' -import type { ParseNeedResult, ParsedNeedCriteria, FollowUpQuestion } from '../domain/needBuilder' -import type { UnifiedMatchResult } from '../domain/unifiedResult' - -// ── Decision Brief ──────────────────────────────────────────────────────────── - -export interface DecisionBrief { - id: string - shortlistId: string - summary: string - sections: { title: string; body: string }[] - generatedAt: string - isDraft: true -} - -function buildMockDecisionBrief(shortlistId: string): DecisionBrief { - return { - id: crypto.randomUUID(), - shortlistId, - summary: 'Die Shortlist enthält qualitativ hochwertige Matches mit starker Standortübereinstimmung. Die verfügbaren Flächen decken den Bedarf gut ab. Zwei Objekte eignen sich als Erstbesichtigungen.', - sections: [ - { - title: 'Zusammenfassung der Objekte', - body: 'Die Shortlist umfasst mehrere Objekte aus dem verifizierten Portfolio. Die Matchscores liegen zwischen 74 und 88, was auf eine gute bis sehr gute Übereinstimmung mit den Suchkriterien hinweist.', - }, - { - title: 'Standortbewertung', - body: 'Die Mehrheit der Objekte befindet sich in bevorzugten Lagen. Die ÖV-Anbindung ist bei allen Objekten als gut bis sehr gut einzustufen.', - }, - { - title: 'Budgetanalyse', - body: 'Die Mietpreise liegen im budgetkonformen Bereich. Keine der Optionen überschreitet das maximale Budget pro m².', - }, - { - title: 'Empfohlene nächste Schritte', - body: '1. Besichtigung der Top-2-Objekte vereinbaren. 2. Detaillierte Flächenpläne anfordern. 3. Vertragskonditionen prüfen lassen.', - }, - ], - generatedAt: new Date().toISOString(), - isDraft: true, - } -} - -// ── Compare Summary ─────────────────────────────────────────────────────────── - -export interface ComparisonSummary { - strongestOption: { matchId: string; label: string; reason: string } - bestValue: { matchId: string; label: string; reason: string } | null - highestConfidence: { matchId: string; label: string; confidenceLevel: number } - biggestTradeoffs: string[] - missingDataWarnings: string[] - recommendedNextStep: string -} - -function buildComparisonSummary(items: UnifiedMatchResult[]): ComparisonSummary { - if (items.length === 0) { - return { - strongestOption: { matchId: '', label: '–', reason: 'Keine Ergebnisse' }, - bestValue: null, - highestConfidence: { matchId: '', label: '–', confidenceLevel: 0 }, - biggestTradeoffs: [], - missingDataWarnings: [], - recommendedNextStep: 'Suchergebnisse überprüfen', - } - } - - const getTitle = (item: UnifiedMatchResult) => - item.resultType !== 'FUTURE_AVAILABILITY' - ? (item as any).property?.title ?? `Match ${item.matchScore}` - : (item as any).signal?.companyName ?? 'Zukunftssignal' - - const strongest = items.reduce((a, b) => a.matchScore > b.matchScore ? a : b) - - const propertyItems = items.filter(i => i.resultType !== 'FUTURE_AVAILABILITY') - const bestValue = propertyItems.length > 0 - ? propertyItems.reduce((a, b) => - ((a as any).property?.rentPricePerSqm ?? Infinity) <= ((b as any).property?.rentPricePerSqm ?? Infinity) ? a : b - ) - : null - - const highestConf = items.reduce((a, b) => - a.match.confidenceLevel >= b.match.confidenceLevel ? a : b - ) - - const tradeoffs = items - .flatMap(i => i.match.tradeoffs?.slice(0, 1).map(t => `${getTitle(i)}: ${t.concern}`) ?? []) - .slice(0, 3) - - const missingWarnings = items - .filter(i => (i.match.missingData?.filter(m => m.importance === 'CRITICAL').length ?? 0) > 0) - .map(i => `${getTitle(i)}: fehlende Pflichtfelder`) - - const topNextAction = strongest.match.nextBestActions?.[0]?.label ?? 'Objekt besichtigen oder Details prüfen' - - return { - strongestOption: { - matchId: strongest.matchId, - label: getTitle(strongest), - reason: `Höchster Match Score (${strongest.matchScore}/100)`, - }, - bestValue: bestValue - ? { - matchId: bestValue.matchId, - label: getTitle(bestValue), - reason: `Niedrigster Mietpreis (CHF ${(bestValue as any).property?.rentPricePerSqm ?? '–'}/m²)`, - } - : null, - highestConfidence: { - matchId: highestConf.matchId, - label: getTitle(highestConf), - confidenceLevel: highestConf.match.confidenceLevel, - }, - biggestTradeoffs: tradeoffs, - missingDataWarnings: missingWarnings, - recommendedNextStep: topNextAction, - } -} - -// ── Legacy types (kept for backward compatibility) ──────────────────────────── - -export interface CriteriaExtractionResult { - extractedCriteria: Partial - confidence: number - missingFields: string[] - assumptions: string[] - followUpQuestions: string[] -} - -export interface AIServiceProvider { - extractCriteria(naturalLanguageInput: string): Promise - generateFollowUp(partialNeed: Partial): Promise -} - -// ── Mock parse logic ────────────────────────────────────────────────────────── - -function mockParseNeed(input: string): ParseNeedResult { - const lower = input.toLowerCase() - - // Asset type - const assetType: AssetType | undefined = - lower.includes('büro') || lower.includes('office') ? 'OFFICE' - : lower.includes('logistik') || lower.includes('lager') ? 'LOGISTICS' - : lower.includes('retail') || lower.includes('laden') || lower.includes('shop') ? 'RETAIL' - : lower.includes('produktion') || lower.includes('gewerbe') || lower.includes('industrie') ? 'PRODUCTION' - : lower.includes('gastro') || lower.includes('restaurant') ? 'GASTRO' - : undefined - - // Area - const areaRangeMatch = input.match(/(\d+)\s*[–\-–]\s*(\d+)\s*m[²2]/i) - const areaSingleMatch = input.match(/(\d{3,5})\s*m[²2]/i) - let areaRange: { min: number; max: number } | undefined - let areaConfidence = 0.25 - if (areaRangeMatch) { - areaRange = { min: parseInt(areaRangeMatch[1]), max: parseInt(areaRangeMatch[2]) } - areaConfidence = 0.95 - } else if (areaSingleMatch) { - const base = parseInt(areaSingleMatch[1]) - areaRange = { min: Math.round(base * 0.8), max: Math.round(base * 1.2) } - areaConfidence = 0.70 - } - - // Locations - const CITIES: [string, string][] = [ - ['zürich', 'Zürich'], ['basel', 'Basel'], ['bern', 'Bern'], ['genf', 'Genf'], - ['lausanne', 'Lausanne'], ['winterthur', 'Winterthur'], ['zug', 'Zug'], - ['luzern', 'Luzern'], ['st. gallen', 'St. Gallen'], ['lugano', 'Lugano'], - ['biel', 'Biel'], ['schaffhausen', 'Schaffhausen'], - ] - const preferredLocations = CITIES.filter(([k]) => lower.includes(k)).map(([, v]) => v) - const locationConfidence = preferredLocations.length > 0 ? 0.88 : 0.15 - - // Budget - const budgetPerSqmMatch = input.match(/(\d+)\s*(?:CHF)?\s*\/\s*m[²2]/i) - const budgetMaxMatch = input.match(/(?:max\.?|bis|höchstens)\s*(?:CHF\s*)?(\d+)/i) - let budgetRange: { maxPerSqm: number; currency: string } | undefined - let budgetConfidence = 0.20 - if (budgetPerSqmMatch) { - budgetRange = { maxPerSqm: parseInt(budgetPerSqmMatch[1]), currency: 'CHF' } - budgetConfidence = 0.92 - } else if (budgetMaxMatch) { - budgetRange = { maxPerSqm: parseInt(budgetMaxMatch[1]), currency: 'CHF' } - budgetConfidence = 0.60 - } - - // Timing - const yearMatch = input.match(/(?:ab\s+)?(?:Q[1-4]\s*\/?\s*)?(\d{4})/) - const soonMatch = lower.includes('sofort') || lower.includes('asap') - let timing: ParsedNeedCriteria['timing'] | undefined - let timingConfidence = 0.20 - if (soonMatch) { - timing = { earliestMoveIn: '2025-07-01', latestMoveIn: '2025-10-01', flexibleTiming: false } - timingConfidence = 0.85 - } else if (yearMatch) { - timing = { earliestMoveIn: `${yearMatch[1]}-01-01`, latestMoveIn: `${yearMatch[1]}-12-31`, flexibleTiming: lower.includes('flexibel') } - timingConfidence = 0.75 - } - - // Must-haves - const mustHaveCriteria: string[] = [] - if (lower.includes('öv') || lower.includes('bahnhof') || lower.includes('tram')) mustHaveCriteria.push('Gute ÖV-Anbindung') - if (lower.includes('parking') || lower.includes('parkplatz') || lower.includes('tiefgarage')) mustHaveCriteria.push('Parkplätze vorhanden') - if (lower.includes('klimaanlage') || lower.includes('klima')) mustHaveCriteria.push('Klimaanlage') - if (lower.includes('server') || lower.includes('rechenzentr')) mustHaveCriteria.push('Serverraum / IT-Infrastruktur') - if (lower.includes('barrierefrei')) mustHaveCriteria.push('Barrierefreiheit') - if (lower.includes('küche') || lower.includes('kantine')) mustHaveCriteria.push('Kantine / Küche') - - // Soft - const prestigeImportance: 'LOW' | 'MEDIUM' | 'HIGH' | undefined = - lower.includes('prestige') || lower.includes('representativ') || lower.includes('repräsentativ') ? 'HIGH' - : lower.includes('standard') ? 'LOW' - : undefined - const parkingNeed = lower.includes('parking') || lower.includes('parkplatz') - const visibilityNeed: 'HIGH' | undefined = lower.includes('sichtbar') || lower.includes('passanten') ? 'HIGH' : undefined - const footfallNeed: 'HIGH' | undefined = lower.includes('frequenz') || lower.includes('laufkundschaft') ? 'HIGH' : undefined - - // Missing fields - const missingFields: string[] = [] - if (!assetType) missingFields.push('Nutzungstyp') - if (!areaRange) missingFields.push('Flächenbedarf') - if (preferredLocations.length === 0) missingFields.push('Standort') - if (!budgetRange) missingFields.push('Budget') - if (!timing) missingFields.push('Verfügbarkeitstermin') - - // Assumptions - const assumptions: string[] = [] - if (areaRange && areaSingleMatch && !areaRangeMatch) { - assumptions.push(`Flächenrange aus Einzelangabe (${areaSingleMatch[1]} m²) geschätzt — bitte prüfen`) - } - if (budgetRange && !budgetPerSqmMatch && budgetMaxMatch) { - assumptions.push('Budget als Pauschalangabe interpretiert — Angabe pro m² unklar') - } - if (!assetType) { - assumptions.push('Nutzungstyp konnte nicht eindeutig erkannt werden') - } - - // Confidence by field - const confidenceByField: Record = { - assetType: assetType ? 0.92 : 0.20, - areaRange: areaConfidence, - preferredLocations: locationConfidence, - budgetRange: budgetConfidence, - timing: timingConfidence, - mustHaveCriteria: mustHaveCriteria.length > 0 ? 0.85 : 0.10, - prestigeImportance: prestigeImportance ? 0.80 : 0.20, - parkingNeed: parkingNeed ? 0.90 : 0.30, - } - - // Follow-up questions - const followUpQuestionCandidates: FollowUpQuestion[] = [] - - if (!assetType) { - followUpQuestionCandidates.push({ - id: 'fq-asset-type', - questionText: 'Welchen Nutzungstyp suchen Sie?', - targetField: 'assetType', - reason: 'Der Nutzungstyp konnte nicht eindeutig erkannt werden.', - suggestedAnswerOptions: ['Büro', 'Logistik / Lager', 'Retail', 'Produktion', 'Gastro / F&B'], - importance: 'required', - }) - } - if (preferredLocations.length === 0) { - followUpQuestionCandidates.push({ - id: 'fq-location', - questionText: 'In welcher Region oder Stadt suchen Sie?', - targetField: 'preferredLocations', - reason: 'Kein konkreter Standort angegeben.', - suggestedAnswerOptions: ['Zürich', 'Basel', 'Bern', 'Zug', 'Luzern', 'Genf'], - importance: 'required', - }) - } - if (!timing) { - followUpQuestionCandidates.push({ - id: 'fq-timing', - questionText: 'Ab wann benötigen Sie die Fläche?', - targetField: 'timing', - reason: 'Kein Verfügbarkeitsdatum erkannt.', - suggestedAnswerOptions: ['Sofort', 'In 3 Monaten', 'In 6 Monaten', 'In 12 Monaten', 'Flexibel'], - importance: 'recommended', - }) - } - if (!budgetRange) { - followUpQuestionCandidates.push({ - id: 'fq-budget', - questionText: 'Was ist Ihr Maximalbudget pro m² und Jahr?', - targetField: 'budgetRange', - reason: 'Kein Budget erkannt.', - suggestedAnswerOptions: ['< CHF 20/m²', 'CHF 20–40/m²', 'CHF 40–80/m²', '> CHF 80/m²', 'Flexible'], - importance: 'recommended', - }) - } - followUpQuestionCandidates.push({ - id: 'fq-parking', - questionText: 'Benötigen Sie Parkplätze vor Ort?', - targetField: 'parkingNeed', - reason: 'Angabe zu Parkplatzbedarf verbessert die Matchqualität.', - suggestedAnswerOptions: ['Ja, zwingend', 'Ja, wenn möglich', 'Nein'], - importance: 'optional', - }) - - // Suggested weights - const suggestedWeights: Record = { - area: 0.20, - location: preferredLocations.length > 0 ? 0.28 : 0.22, - budget: budgetRange ? 0.22 : 0.18, - timing: timing ? 0.15 : 0.12, - prestige: prestigeImportance === 'HIGH' ? 0.10 : 0.05, - accessibility: mustHaveCriteria.includes('Gute ÖV-Anbindung') ? 0.08 : 0.04, - expansionPotential: 0.03, - flexibility: lower.includes('flexibel') ? 0.07 : 0.03, - } - - const rawSummary = `Bedarf analysiert: ${assetType ?? 'Typ unbekannt'} · ${preferredLocations.join(', ') || 'Standort unklar'} · ${areaRange ? `${areaRange.min}–${areaRange.max} m²` : 'Fläche unklar'} · ${budgetRange ? `max. CHF ${budgetRange.maxPerSqm}/m²` : 'Budget unklar'}` - - return { - extractedCriteria: { - assetType, - areaRange, - preferredLocations, - budgetRange, - timing, - mustHaveCriteria, - infrastructureRequirements: [], - accessibilityRequirements: [], - prestigeImportance, - flexibilityNeed: lower.includes('flexibel') ? 'HIGH' : 'MEDIUM', - expansionPotential: lower.includes('wachstum') || lower.includes('expansion'), - parkingNeed, - visibilityNeed, - footfallNeed, - }, - confidenceByField, - missingFields, - assumptions, - suggestedWeights, - followUpQuestionCandidates, - rawSummary, - promptVersion: 'mock-v1.0', - schemaVersion: '1.0.0', - } -} - -// ── Legacy mock provider (kept for backward compat) ─────────────────────────── - -const MockupAIServiceProvider: AIServiceProvider = { - async extractCriteria(_input: string): Promise { - 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 - -const notConfiguredError = (): ServiceError => ({ - code: ServiceErrorCode.AI_GENERATION_FAILED, - message: 'OpenRouter nicht konfiguriert', -}) - -export const openRouterAIService: AIServiceProvider = { - async extractCriteria(_input: string): Promise { - throw notConfiguredError() - }, - async generateFollowUp(_partialNeed: Partial): Promise { - throw notConfiguredError() - }, -} - -export const aiService = { - // Legacy methods - 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 } - }, - - // F014: Compare summary - async summarizeComparison(items: UnifiedMatchResult[]): Promise> { - await new Promise(r => setTimeout(r, 600)) - return { data: buildComparisonSummary(items) } - }, - - // F008 methods - async parseNeed(input: string): Promise> { - await new Promise(r => setTimeout(r, 1400)) - const data = mockParseNeed(input) - return { data } - }, - async generateFollowUpQuestions(criteria: ParsedNeedCriteria): Promise> { - await new Promise(r => setTimeout(r, 600)) - const result = mockParseNeed(JSON.stringify(criteria)) - return { data: result.followUpQuestionCandidates } - }, - - async generateDecisionBrief(shortlistId: string): Promise> { - await new Promise(r => setTimeout(r, 1800)) - return { data: buildMockDecisionBrief(shortlistId) } - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/authService.ts b/.claude/worktrees/agent-a82a3716/src/services/authService.ts deleted file mode 100644 index 0dc9a70..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/authService.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { useSessionStore } from '../stores/sessionStore' -import type { MockUser } from '../stores/sessionStore' -import type { ItemResponse } from './types' -import { UserRole, WorkspaceType } from '../domain/enums' -import { getPermissions, getAccessibleWorkspaces } from '../lib/permissions' -import type { Permission } from '../lib/permissions' - -// Mock organizations for org switching -const MOCK_ORGANIZATIONS: { id: string; name: string }[] = [ - { id: 'org-wincasa', name: 'Wincasa AG' }, - { id: 'org-mobimo', name: 'Mobimo Management AG' }, - { id: 'org-ubs', name: 'UBS Asset Management RE' }, -] - -// Demo user presets per role -const DEMO_USERS: Record = { - [UserRole.SUPER_ADMIN]: { - id: 'user-super', - email: 'super@ideal-sharing.ch', - name: 'Super Admin', - role: UserRole.SUPER_ADMIN, - organizationId: 'org-wincasa', - organizationName: 'Wincasa AG', - allowedWorkspaces: [WorkspaceType.SUPPLY, WorkspaceType.DEMAND, WorkspaceType.OPERATIONS], - }, - [UserRole.ORGANIZATION_ADMIN]: { - id: 'user-001', - email: 'admin@ideal-sharing.ch', - name: 'Admin User', - role: UserRole.ORGANIZATION_ADMIN, - organizationId: 'org-wincasa', - organizationName: 'Wincasa AG', - allowedWorkspaces: [WorkspaceType.SUPPLY, WorkspaceType.DEMAND], - }, - [UserRole.PROPERTY_MANAGER]: { - id: 'user-pm', - email: 'pm@ideal-sharing.ch', - name: 'Property Manager', - role: UserRole.PROPERTY_MANAGER, - organizationId: 'org-wincasa', - organizationName: 'Wincasa AG', - allowedWorkspaces: getAccessibleWorkspaces(UserRole.PROPERTY_MANAGER), - }, - [UserRole.REVIEWER]: { - id: 'user-rev', - email: 'reviewer@ideal-sharing.ch', - name: 'Reviewer', - role: UserRole.REVIEWER, - organizationId: 'org-wincasa', - organizationName: 'Wincasa AG', - allowedWorkspaces: getAccessibleWorkspaces(UserRole.REVIEWER), - }, - [UserRole.OWNER_VIEWER]: { - id: 'user-ov', - email: 'owner@ideal-sharing.ch', - name: 'Owner Viewer', - role: UserRole.OWNER_VIEWER, - organizationId: 'org-wincasa', - organizationName: 'Wincasa AG', - allowedWorkspaces: getAccessibleWorkspaces(UserRole.OWNER_VIEWER), - }, - [UserRole.DEMAND_USER]: { - id: 'user-dem', - email: 'demand@ideal-sharing.ch', - name: 'Demand User', - role: UserRole.DEMAND_USER, - organizationId: 'org-mobimo', - organizationName: 'Mobimo Management AG', - allowedWorkspaces: getAccessibleWorkspaces(UserRole.DEMAND_USER), - }, -} - -export const authService = { - async getCurrentUser(): Promise> { - const data = useSessionStore.getState().currentUser - return { data } - }, - - async getCurrentOrganization(): Promise> { - const { activeOrganizationId } = useSessionStore.getState() - const org = MOCK_ORGANIZATIONS.find((o) => o.id === activeOrganizationId) ?? null - return { data: org } - }, - - async login(email: string, _password: string): Promise> { - const existing = Object.values(DEMO_USERS).find((u) => u.email === email) - const user: MockUser = existing ?? { - id: 'user-001', - email, - name: 'Admin User', - role: UserRole.ORGANIZATION_ADMIN, - organizationId: 'org-wincasa', - organizationName: 'Wincasa AG', - allowedWorkspaces: [WorkspaceType.SUPPLY, WorkspaceType.DEMAND, WorkspaceType.OPERATIONS], - } - useSessionStore.getState().login(user) - return { data: user } - }, - - async logout(): Promise> { - useSessionStore.getState().logout() - return { data: undefined } - }, - - async isAuthenticated(): Promise> { - const data = useSessionStore.getState().isAuthenticated - return { data } - }, - - async switchDemoRole(role: UserRole): Promise> { - const user = DEMO_USERS[role] - useSessionStore.getState().login(user) - return { data: user } - }, - - async switchOrganization(organizationId: string): Promise> { - const org = MOCK_ORGANIZATIONS.find((o) => o.id === organizationId) - if (org) { - const state = useSessionStore.getState() - if (state.currentUser) { - state.login({ ...state.currentUser, organizationId: org.id, organizationName: org.name }) - } else { - state.switchOrganization(organizationId) - } - } - return { data: undefined } - }, - - async getPermissions(user: MockUser): Promise> { - return { data: getPermissions(user) } - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/dashboardService.ts b/.claude/worktrees/agent-a82a3716/src/services/dashboardService.ts deleted file mode 100644 index d51df6a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/dashboardService.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { propertyService } from './propertyService' -import { matchService } from './matchService' -import { futureSignalService } from './futureSignalService' -import { dataQualityService } from './dataQualityService' -import { reviewService } from './reviewService' -import type { DashboardData } from '../domain/dashboard' - -export const dashboardService = { - async getDashboardData(): Promise { - const [propRes, matchRes, signalRes, qualityRes, reviewRes] = await Promise.allSettled([ - propertyService.getDashboardPropertiesSummary(), - matchService.getStrongMatches(), - futureSignalService.getSignalSummary(), - dataQualityService.getPortfolioQualitySummary(), - reviewService.getDashboardTasks(), - ]) - - const propSummary = propRes.status === 'fulfilled' ? propRes.value : null - const strongMatches = matchRes.status === 'fulfilled' ? matchRes.value : null - const signals = signalRes.status === 'fulfilled' ? signalRes.value : null - const quality = qualityRes.status === 'fulfilled' ? qualityRes.value : null - const tasks = reviewRes.status === 'fulfilled' ? reviewRes.value : null - - return { - totalProperties: propSummary?.total ?? 0, - activeProperties: propSummary?.active ?? 0, - strongMatchCount: strongMatches?.length ?? 0, - avgDataQuality: quality?.avgScore ?? 0, - futureSignals: signals, - dataQuality: quality, - reviewTasks: tasks, - strongMatches, - lastUpdated: new Date().toISOString(), - } - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/dataQualityService.ts b/.claude/worktrees/agent-a82a3716/src/services/dataQualityService.ts deleted file mode 100644 index dcacbb8..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/dataQualityService.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { MockupPropertyProvider } from '../provider/MockupPropertyProvider' -import { FreshnessStatus } from '../domain/enums' -import type { DataQualitySummary } from '../domain/dashboard' -import type { DataQuality, Property } from '../domain/property' - -export type RecommendedAction = { - id: string - label: string - detail: string - priority: 'HIGH' | 'MEDIUM' | 'LOW' - field?: string -} - -// ── Field → Action map ──────────────────────────────────────────────────────── - -const FIELD_ACTION_MAP: Record> = { - 'Mietpreis/m²': { label: 'Mietpreis ergänzen', detail: 'Fehlender Mietpreis schließt Objekt aus Budget-Matches aus', priority: 'HIGH', field: 'Mietpreis/m²' }, - 'Fläche m²': { label: 'Fläche bestätigen', detail: 'Fläche ist Hard-Kriterium für alle Matchings', priority: 'HIGH', field: 'Fläche m²' }, - 'Verfügbarkeit': { label: 'Verfügbarkeit bestätigen', detail: 'Timing ist entscheidend für Nachfrager mit Deadlines', priority: 'HIGH', field: 'Verfügbarkeit' }, - 'Adresse': { label: 'Adresse vervollständigen', detail: 'Für Standortbewertung und Kartenansicht notwendig', priority: 'HIGH', field: 'Adresse' }, - 'Beschreibung': { label: 'Beschreibung hinzufügen', detail: 'Verbesserter Kontext erhöht Nachfrager-Vertrauen', priority: 'MEDIUM', field: 'Beschreibung' }, - 'Soft Factors': { label: 'Passantenfrequenz & ESG', detail: 'Soft Factors verbessern Match-Scoring erheblich', priority: 'MEDIUM', field: 'Soft Factors' }, - 'Ausbaustandard': { label: 'Ausbaustandard angeben', detail: 'SHELL/BASIC/FULL/PREMIUM beeinflusst Eignung stark', priority: 'MEDIUM', field: 'Ausbaustandard' }, - 'Bilder': { label: 'Bilder hochladen', detail: 'Objektfotos steigern Anfragerate deutlich', priority: 'MEDIUM', field: 'Bilder' }, - 'Jahresmiete (CHF)': { label: 'Jahresmiete angeben', detail: 'Ergänzt Mietpreis/m² für Budgetvergleiche', priority: 'LOW', field: 'Jahresmiete (CHF)' }, - 'Expansionspotenzial': { label: 'Erweiterungsfläche angeben', detail: 'Wichtig für wachsende Unternehmen', priority: 'LOW', field: 'Expansionspotenzial' }, -} - -const FRESHNESS_ACTIONS: Record = { - [FreshnessStatus.OUTDATED]: { - id: 'review_source', - label: 'Quelle überprüfen', - detail: 'Daten sind älter als 14 Tage — Verfügbarkeit könnte sich geändert haben', - priority: 'HIGH', - }, - [FreshnessStatus.STALE]: { - id: 'update_data', - label: 'Daten aktualisieren', - detail: 'Daten sind 2–14 Tage alt — Aktualitätsscore reduziert', - priority: 'MEDIUM', - }, -} - -// ── Core Checks ─────────────────────────────────────────────────────────────── - -const CRITICAL_CHECKS: Array<{ field: string; present: (p: Property) => boolean }> = [ - { field: 'Mietpreis/m²', present: p => p.rentPricePerSqm > 0 }, - { field: 'Fläche m²', present: p => p.areaSqm > 0 }, - { field: 'Verfügbarkeit', present: p => !!p.availabilityDate }, - { field: 'Adresse', present: p => !!p.address?.street && !!p.address?.city }, -] - -const OPTIONAL_CHECKS: Array<{ field: string; present: (p: Property) => boolean }> = [ - { field: 'Beschreibung', present: p => !!p.description && p.description.length > 20 }, - { field: 'Soft Factors', present: p => !!(p.softFactors?.prestigeScore || p.softFactors?.footfallScore || p.softFactors?.commuterAccessScore) }, - { field: 'Ausbaustandard', present: p => !!p.hardFacts?.fitOut }, - { field: 'Bilder', present: p => (p.images?.length ?? 0) > 0 }, - { field: 'Jahresmiete (CHF)', present: p => !!p.rentChfSqmYear }, - { field: 'Expansionspotenzial', present: p => !!(p.expansionPotentialSqm || p.hardFacts) }, -] - -// ── Public API ──────────────────────────────────────────────────────────────── - -export function getMissingCriticalFields(property: Property): string[] { - const fromData = property.dataQuality?.missingCriticalFields ?? [] - if (fromData.length > 0) return fromData - return CRITICAL_CHECKS.filter(c => !c.present(property)).map(c => c.field) -} - -export function getQualityWarnings(property: Property): string[] { - return property.dataQuality?.warnings ?? [] -} - -export function getRecommendedActions(quality: DataQuality, freshness?: string): RecommendedAction[] { - const actions: RecommendedAction[] = [] - - for (const field of quality.missingCriticalFields) { - const def = FIELD_ACTION_MAP[field] - if (def) actions.push({ id: `fill_${field}`, ...def }) - } - - const fn = freshness ?? quality.freshness - if (fn && fn !== FreshnessStatus.FRESH) { - const freshnessAction = FRESHNESS_ACTIONS[fn] - if (freshnessAction) actions.push(freshnessAction) - } - - for (const field of quality.missingOptionalFields) { - const def = FIELD_ACTION_MAP[field] - if (def) actions.push({ id: `fill_opt_${field}`, ...def }) - } - - return actions -} - -export function calculatePropertyQuality(property: Property): DataQuality { - if (property.dataQuality?.qualityLevel) return property.dataQuality - - const missingCritical = CRITICAL_CHECKS.filter(c => !c.present(property)).map(c => c.field) - const missingOptional = OPTIONAL_CHECKS.filter(c => !c.present(property)).map(c => c.field) - - const completeness = 1 - (missingCritical.length * 0.15 + missingOptional.length * 0.05) - const confidence = property.confidenceScore ?? 0.5 - - const freshnessVal = property.dataQuality?.freshness ?? FreshnessStatus.OUTDATED - const freshnessFactor = freshnessVal === FreshnessStatus.FRESH ? 1 : freshnessVal === FreshnessStatus.STALE ? 0.7 : 0.4 - - const score = Math.min(1, Math.max(0, completeness * 0.5 + confidence * 0.3 + freshnessFactor * 0.2)) - - const warnings: string[] = [] - if (confidence < 0.5) warnings.push('Niedrige Daten-Vertrauensscore') - if (freshnessVal === FreshnessStatus.OUTDATED) warnings.push('Daten sind veraltet (>14 Tage)') - if (missingCritical.length > 0) warnings.push(`${missingCritical.length} Pflichtfeld(er) fehlen`) - - const qualityLevel = missingCritical.length > 0 - ? 'INCOMPLETE' - : score >= 0.8 ? 'HIGH' : score >= 0.6 ? 'MEDIUM' : 'LOW' - - return { - score, - qualityLevel, - missingCriticalFields: missingCritical, - missingOptionalFields: missingOptional, - lastVerifiedAt: property.dataQuality?.lastVerifiedAt, - freshness: freshnessVal, - warnings, - } -} - -// ── Portfolio summary (existing) ────────────────────────────────────────────── - -export const dataQualityService = { - async getPortfolioQualitySummary(): Promise { - const properties = await MockupPropertyProvider.getAll() - - const avgScoreRaw = - properties.length > 0 - ? properties.reduce((sum, p) => sum + (p.dataQuality?.score ?? 0), 0) / properties.length - : 0 - - const fieldCounts = properties - .flatMap(p => p.dataQuality?.missingCriticalFields ?? []) - .reduce>((acc, f) => { - acc[f] = (acc[f] ?? 0) + 1 - return acc - }, {}) - - const topMissingFields = Object.entries(fieldCounts) - .sort((a, b) => b[1] - a[1]) - .slice(0, 5) - .map(([f]) => f) - - return { - avgScore: Math.round(avgScoreRaw * 100), - critical: properties.filter(p => (p.dataQuality?.score ?? 0) < 0.5).length, - propertiesWithMissingCritical: properties.filter( - p => (p.dataQuality?.missingCriticalFields?.length ?? 0) > 0, - ).length, - topMissingFields, - } - }, - - getMissingCriticalFields, - getQualityWarnings, - getRecommendedActions, - calculatePropertyQuality, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/futureSignalService.ts b/.claude/worktrees/agent-a82a3716/src/services/futureSignalService.ts deleted file mode 100644 index 3e9d43f..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/futureSignalService.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { MockupFutureSignalProvider } from '../provider/MockupFutureSignalProvider' -import type { FutureSignalFilters } from '../provider/IFutureSignalProvider' -import type { FutureSignal } from '../domain/futureSignal' -import type { ReviewStatus } from '../domain/enums' -import type { FutureSignalSummary } from '../domain/dashboard' -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 } - }, - async updateReviewStatus(id: string, status: ReviewStatus): Promise> { - const data = await provider.updateReviewStatus(id, status) - return { data } - }, - - async getSignalsForProperty(propertyId: string): Promise> { - const data = await provider.getByProperty(propertyId) - return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } - }, - - async getSignalSummary(): Promise { - const signals = await provider.getAll() - const RESTRICTED: readonly string[] = ['CONFIDENTIAL', 'INTERNAL'] - return { - total: signals.length, - highConfidence: signals.filter(s => s.confidenceScore >= 0.75).length, - restricted: signals.filter(s => RESTRICTED.includes(s.sensitivityLevel)).length, - needsReview: signals.filter(s => !s.isVerified).length, - avgTimeHorizonMonths: - signals.length > 0 - ? Math.round(signals.reduce((sum, s) => sum + (s.timeHorizonMonths ?? 0), 0) / signals.length) - : 0, - timeHorizonDistribution: { - short: signals.filter(s => (s.timeHorizonMonths ?? 0) <= 6).length, - medium: signals.filter(s => { const m = s.timeHorizonMonths ?? 0; return m > 6 && m <= 12 }).length, - long: signals.filter(s => (s.timeHorizonMonths ?? 0) > 12).length, - }, - } - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/governanceService.ts b/.claude/worktrees/agent-a82a3716/src/services/governanceService.ts deleted file mode 100644 index 5409832..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/governanceService.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { ListResponse, ItemResponse } from './types' - -export type ActivityEventType = - | 'PROPERTY_CREATED' - | 'PROPERTY_UPDATED' - | 'MATCH_APPROVED' - | 'MATCH_REJECTED' - | 'SIGNAL_VERIFIED' - | 'NEED_CREATED' - | 'REVIEW_REQUESTED' - | 'AI_PARSE_COMPLETED' - | 'AI_OUTPUT_REVIEWED' - | 'MATCH_GENERATED' - | 'FUTURE_SIGNAL_DETECTED' - | 'FUTURE_SIGNAL_CONVERTED' - | 'SHORTLIST_CREATED' - | 'SHORTLIST_FINALIZED' - | 'DECISION_BRIEF_CREATED' - | 'SOURCE_CRAWLED' - | 'DATA_QUALITY_FLAGGED' - | 'REVIEW_COMPLETED' - -export type ActivityCategory = 'SUCHE' | 'MATCHING' | 'INTELLIGENCE' | 'REVIEW' | 'GOVERNANCE' - -export interface ActivityEvent { - id: string - type: ActivityEventType - category: ActivityCategory - entityId: string - entityType: 'PROPERTY' | 'MATCH' | 'NEED' | 'SIGNAL' | 'SHORTLIST' | 'AI_OUTPUT' | 'SOURCE' - performedBy: string - isAiAction: boolean - organizationId: string - notes?: string - createdAt: string -} - -const mockActivityLog: ActivityEvent[] = [ - // Day 1 — 2026-05-13 - { id: 'evt-001', type: 'SOURCE_CRAWLED', category: 'INTELLIGENCE', entityId: 'src-001', entityType: 'SOURCE', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Baubewilligung-Feed Kanton ZH: 47 neue Einträge gefunden', createdAt: '2026-05-13T06:15:00Z' }, - { id: 'evt-002', type: 'FUTURE_SIGNAL_DETECTED', category: 'INTELLIGENCE', entityId: 'sig-maag', entityType: 'SIGNAL', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Potenzielle Grossfläche: Maag Areal Zürich — Wahrscheinlichkeit 82%', createdAt: '2026-05-13T06:18:00Z' }, - { id: 'evt-003', type: 'DATA_QUALITY_FLAGGED', category: 'GOVERNANCE', entityId: 'prop-007', entityType: 'PROPERTY', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Grundriss fehlt; Qualitätsscore 61 — unter Schwellenwert', createdAt: '2026-05-13T07:30:00Z' }, - { id: 'evt-004', type: 'PROPERTY_CREATED', category: 'GOVERNANCE', entityId: 'prop-022', entityType: 'PROPERTY', performedBy: 'manager@wincasa.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Neues Industrieobjekt Schlieren erfasst', createdAt: '2026-05-13T09:45:00Z' }, - // Day 2 — 2026-05-14 - { id: 'evt-005', type: 'SOURCE_CRAWLED', category: 'INTELLIGENCE', entityId: 'src-002', entityType: 'SOURCE', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Handelsregister-Crawler: 12 Unternehmensumzüge identifiziert', createdAt: '2026-05-14T06:00:00Z' }, - { id: 'evt-006', type: 'FUTURE_SIGNAL_DETECTED', category: 'INTELLIGENCE', entityId: 'sig-alstom', entityType: 'SIGNAL', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Alstom AG Standortanalyse — interne Dokumente verweisen auf Expansionspläne', createdAt: '2026-05-14T06:05:00Z' }, - { id: 'evt-007', type: 'PROPERTY_UPDATED', category: 'GOVERNANCE', entityId: 'prop-003', entityType: 'PROPERTY', performedBy: 'manager@wincasa.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Verfügbarkeit aktualisiert: sofort verfügbar', createdAt: '2026-05-14T10:20:00Z' }, - { id: 'evt-008', type: 'REVIEW_REQUESTED', category: 'REVIEW', entityId: 'ai-out-003', entityType: 'AI_OUTPUT', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Need-Parse-Output zur manuellen Prüfung eingereicht', createdAt: '2026-05-14T14:00:00Z' }, - { id: 'evt-009', type: 'AI_OUTPUT_REVIEWED', category: 'REVIEW', entityId: 'ai-out-003', entityType: 'AI_OUTPUT', performedBy: 'reviewer@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Output genehmigt — Kriterienextraktion korrekt', createdAt: '2026-05-14T15:30:00Z' }, - // Day 3 — 2026-05-15 - { id: 'evt-010', type: 'NEED_CREATED', category: 'SUCHE', entityId: 'need-042', entityType: 'NEED', performedBy: 'demand@globocorp.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Natürlichsprachliche Suche: "2500m² Büro Zürich West, offen, Rep.'+ "'" + 'resentanz-qualität"', createdAt: '2026-05-15T09:10:00Z' }, - { id: 'evt-011', type: 'AI_PARSE_COMPLETED', category: 'SUCHE', entityId: 'need-042', entityType: 'NEED', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Konfidenz 87% — Kriterien: OFFICE 2000–3000m², Zürich West, CHF 280/m², Einzug Q3 2026', createdAt: '2026-05-15T09:10:04Z' }, - { id: 'evt-012', type: 'MATCH_GENERATED', category: 'MATCHING', entityId: 'need-042', entityType: 'NEED', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: '14 Kandidaten bewertet — 3 STARK (≥80), 6 MITTEL, 5 SCHWACH', createdAt: '2026-05-15T09:10:07Z' }, - { id: 'evt-013', type: 'SIGNAL_VERIFIED', category: 'INTELLIGENCE', entityId: 'sig-maag', entityType: 'SIGNAL', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Maag Areal Signal verifiziert — Baubewilligung bestätigt', createdAt: '2026-05-15T10:00:00Z' }, - { id: 'evt-014', type: 'SHORTLIST_CREATED', category: 'SUCHE', entityId: 'sl-globo-01', entityType: 'SHORTLIST', performedBy: 'demand@globocorp.ch', isAiAction: false, organizationId: 'org-wincasa', notes: '4 Objekte auf Shortlist "Zürich West Q3 2026"', createdAt: '2026-05-15T11:45:00Z' }, - { id: 'evt-015', type: 'FUTURE_SIGNAL_CONVERTED', category: 'INTELLIGENCE', entityId: 'sig-maag', entityType: 'SIGNAL', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Signal in zukünftige Verfügbarkeit umgewandelt — erscheint im Unified Feed', createdAt: '2026-05-15T13:00:00Z' }, - // Day 4 — 2026-05-16 - { id: 'evt-016', type: 'MATCH_APPROVED', category: 'MATCHING', entityId: 'match-003', entityType: 'MATCH', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Score 91 — Starker Match Hardturmstrasse 201 × GloboCorp bestätigt', createdAt: '2026-05-16T09:00:00Z' }, - { id: 'evt-017', type: 'REVIEW_REQUESTED', category: 'REVIEW', entityId: 'sig-alstom', entityType: 'SIGNAL', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Vertrauliches Signal zur Governance-Prüfung eingereicht', createdAt: '2026-05-16T10:15:00Z' }, - { id: 'evt-018', type: 'DECISION_BRIEF_CREATED', category: 'SUCHE', entityId: 'sl-globo-01', entityType: 'SHORTLIST', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'KI-Entscheidungsbriefing für Shortlist generiert — Empfehlung: Hardturmstrasse 201', createdAt: '2026-05-16T11:30:00Z' }, - { id: 'evt-019', type: 'REVIEW_COMPLETED', category: 'REVIEW', entityId: 'sig-alstom', entityType: 'SIGNAL', performedBy: 'reviewer@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'GENEHMIGT — Vertraulichkeitsstufe bestätigt, Signal für Demand-Pipeline freigegeben', createdAt: '2026-05-16T14:00:00Z' }, - { id: 'evt-020', type: 'SHORTLIST_FINALIZED', category: 'SUCHE', entityId: 'sl-globo-01', entityType: 'SHORTLIST', performedBy: 'demand@globocorp.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Shortlist finalisiert — Kundenentscheid: Objekt 1 und 3 zur Besichtigung', createdAt: '2026-05-16T16:45:00Z' }, - // Day 5 — 2026-05-17 (heute) - { id: 'evt-021', type: 'SOURCE_CRAWLED', category: 'INTELLIGENCE', entityId: 'src-003', entityType: 'SOURCE', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'LinkedIn-Jobpostings-Crawler: 8 Expansionssignale gefunden', createdAt: '2026-05-17T06:00:00Z' }, - { id: 'evt-022', type: 'FUTURE_SIGNAL_DETECTED', category: 'INTELLIGENCE', entityId: 'sig-novartis', entityType: 'SIGNAL', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Novartis Basel: 47 Stelleninserate für "Basel Life Sciences Hub" — Flächensignal', createdAt: '2026-05-17T06:12:00Z' }, - { id: 'evt-023', type: 'MATCH_REJECTED', category: 'MATCHING', entityId: 'match-009', entityType: 'MATCH', performedBy: 'admin@ideal-sharing.ch', isAiAction: false, organizationId: 'org-wincasa', notes: 'Score 64 — zu schwach, manuell abgelehnt', createdAt: '2026-05-17T08:30:00Z' }, - { id: 'evt-024', type: 'DATA_QUALITY_FLAGGED', category: 'GOVERNANCE', entityId: 'prop-015', entityType: 'PROPERTY', performedBy: 'system@property-match', isAiAction: true, organizationId: 'org-wincasa', notes: 'Preisangabe 18 Monate alt — automatische Qualitätswarnung', createdAt: '2026-05-17T09: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/.claude/worktrees/agent-a82a3716/src/services/marketIntelligenceService.ts b/.claude/worktrees/agent-a82a3716/src/services/marketIntelligenceService.ts deleted file mode 100644 index 39adf27..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/marketIntelligenceService.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { MockupMarketIntelligenceProvider } from '../provider/MockupMarketIntelligenceProvider' -import type { MarketSignal, MarketSignalFilters, SignalProcessingStatus } from '../domain/marketSignal' -import type { ListResponse, ItemResponse } from './types' - -const provider = MockupMarketIntelligenceProvider - -export const marketIntelligenceService = { - async getSignals(filters?: MarketSignalFilters): Promise> { - const data = await provider.getSignals(filters) - return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } - }, - - async getSignalDetail(id: string): Promise> { - const data = await provider.getSignalById(id) - return { data } - }, - - async updateSignalStatus( - id: string, - status: SignalProcessingStatus, - ): Promise> { - const data = await provider.updateSignalStatus(id, status) - return { data } - }, - - async convertToFutureSignal( - id: string, - ): Promise> { - const data = await provider.convertToFutureSignal(id) - return { data } - }, - - async linkSignalToEntity( - id: string, - entityType: 'property' | 'need', - entityId: string, - ): Promise> { - const data = await provider.linkSignalToEntity(id, entityType, entityId) - return { data } - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/matchService.ts b/.claude/worktrees/agent-a82a3716/src/services/matchService.ts deleted file mode 100644 index 3b799c8..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/matchService.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { MockupMatchProvider } from '../provider/MockupMatchProvider' -import { MockupPropertyProvider } from '../provider/MockupPropertyProvider' -import { MockupNeedProvider } from '../provider/MockupNeedProvider' -import type { MatchFilters } from '../provider/IMatchProvider' -import type { Match } from '../domain/match' -import type { Need } from '../domain/need' -import type { Property } from '../domain/property' -import type { StrongMatchItem } from '../domain/dashboard' -import type { ScoreBreakdown } from '../domain/match' -import type { ListResponse, ItemResponse } from './types' -import { buildFullMatch, computeRankedMatches } from '../features/matching/rankingEngine' - -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 } - }, - - async getMatchesForProperty(propertyId: string): Promise> { - const data = await provider.getByProperty(propertyId) - return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } - }, - - async getMatchDetail(id: string): Promise> { - const data = await provider.getById(id) - return { data } - }, - - async getScoreBreakdown(matchId: string): Promise> { - const match = await provider.getById(matchId) - return { data: match?.scoreBreakdown ?? null } - }, - - // ── Engine-based methods ────────────────────────────────────────────────── - - computeMatch(need: Need, property: Property): Match { - return buildFullMatch(need, property) - }, - - async computeMatchesForNeed(needId: string): Promise> { - const [need, properties] = await Promise.all([ - MockupNeedProvider.getById(needId), - MockupPropertyProvider.getAll(), - ]) - if (!need) return { data: [], meta: { total: 0, page: 1, pageSize: 0, hasMore: false } } - const data = computeRankedMatches(need, properties) - return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } - }, - - async getStrongMatches(minScore = 80): Promise { - const [matches, properties] = await Promise.all([ - provider.getAll(), - MockupPropertyProvider.getAll(), - ]) - return matches - .filter(m => m.matchScore >= minScore) - .slice(0, 5) - .map(m => { - const prop = properties.find(p => p.id === m.propertyId) - const topFactor = m.positiveFactors?.[0] - const firstAction = m.nextBestActions?.[0] - return { - matchId: m.id, - propertyId: m.propertyId, - propertyTitle: prop?.title ?? 'Unbekanntes Objekt', - propertyAddress: prop?.address - ? `${prop.address.street} ${prop.address.houseNumber}, ${prop.address.city}` - : '–', - needSummary: m.needId, - matchScore: m.matchScore, - topReason: topFactor?.explanation ?? topFactor?.criterion ?? '–', - missingDataCount: m.missingData?.length ?? 0, - nextBestAction: firstAction?.label ?? '–', - } satisfies StrongMatchItem - }) - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/needService.ts b/.claude/worktrees/agent-a82a3716/src/services/needService.ts deleted file mode 100644 index 6fb0a08..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/needService.ts +++ /dev/null @@ -1,29 +0,0 @@ -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/.claude/worktrees/agent-a82a3716/src/services/propertyService.ts b/.claude/worktrees/agent-a82a3716/src/services/propertyService.ts deleted file mode 100644 index 028cc16..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/propertyService.ts +++ /dev/null @@ -1,44 +0,0 @@ -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 -const ACTIVE_STATUSES: readonly string[] = ['AVAILABLE_NOW', 'AVAILABLE_SOON'] - -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 } - }, - - async getDashboardPropertiesSummary(): Promise<{ total: number; active: number }> { - const data = await provider.getAll() - return { - total: data.length, - active: data.filter(p => ACTIVE_STATUSES.includes(p.availabilityStatus)).length, - } - }, - - async getProperties(filters?: PropertyFilters): Promise> { - const data = await provider.getAll(filters) - return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/reviewService.ts b/.claude/worktrees/agent-a82a3716/src/services/reviewService.ts deleted file mode 100644 index 6ead966..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/reviewService.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { MockupReviewProvider } from '../provider/MockupReviewProvider' -import type { ReviewFilters } from '../provider/IReviewProvider' -import type { ReviewTask, ReviewTaskStatus, ReviewNote } from '../domain/review' -import type { DashboardReviewTask } from '../domain/dashboard' -import type { ListResponse, ItemResponse } from './types' - -const provider = MockupReviewProvider - -export const reviewService = { - async getQueue(filters?: ReviewFilters): Promise> { - const data = await provider.getQueue(filters) - return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } - }, - - async getTasks(filters?: ReviewFilters): Promise> { - return this.getQueue(filters) - }, - - async getById(id: string): Promise> { - const data = await provider.getById(id) - return { data } - }, - - async getTask(id: string): Promise> { - return this.getById(id) - }, - - async updateStatus(id: string, status: ReviewTaskStatus, userId: string, note?: string): Promise> { - const data = await provider.updateStatus(id, status, userId, note) - return { data } - }, - - async addNote(id: string, note: Omit): Promise> { - const data = await provider.addNote(id, note) - return { data } - }, - - async approve(id: string, reviewedBy: string, notes?: string): Promise> { - const data = await provider.approve(id, reviewedBy, notes) - return { data } - }, - - async reject(id: string, reviewedBy: string, notes?: string): Promise> { - const data = await provider.reject(id, reviewedBy, notes) - return { data } - }, - - async assign(id: string, assignTo: string): Promise> { - const data = await provider.assign(id, assignTo) - return { data } - }, - - async createReviewTask(signalId: string): Promise> { - const taskId = `rt-${signalId}-${Date.now()}` - return { data: { taskId } } - }, - - async getDashboardTasks(): Promise { - const items = await provider.getQueue() - return items - .filter(r => r.status === 'PENDING' || r.status === 'IN_REVIEW' || r.status === 'ESCALATED') - .slice(0, 8) - .map(r => ({ - id: r.id, - title: r.title, - priority: r.priority as 'HIGH' | 'MEDIUM' | 'LOW', - status: r.status, - type: r.entityType, - })) - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/shortlistService.ts b/.claude/worktrees/agent-a82a3716/src/services/shortlistService.ts deleted file mode 100644 index 6b8692f..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/shortlistService.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { MockupShortlistProvider } from '../provider/MockupShortlistProvider' -import type { ShortlistFilters } from '../provider/IShortlistProvider' -import type { Shortlist, CreateShortlistInput, UpdateShortlistInput, ShortlistItemInput } from '../domain/shortlist' -import type { ListResponse, ItemResponse } from './types' - -const provider = MockupShortlistProvider - -export const shortlistService = { - async getAll(filters?: ShortlistFilters): 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: CreateShortlistInput): Promise> { - const data = await provider.create(input) - return { data } - }, - async update(id: string, input: UpdateShortlistInput): Promise> { - const data = await provider.update(id, input) - return { data } - }, - async addItem(id: string, item: ShortlistItemInput): Promise> { - const data = await provider.addItem(id, item) - return { data } - }, - async removeItem(id: string, resultId: string): Promise> { - const data = await provider.removeItem(id, resultId) - return { data } - }, - async remove(id: string): Promise> { - await provider.remove(id) - return { data: undefined } - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/signalPipelineService.ts b/.claude/worktrees/agent-a82a3716/src/services/signalPipelineService.ts deleted file mode 100644 index ed4bf5a..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/signalPipelineService.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MockupSignalPipelineProvider } from '../provider/MockupSignalPipelineProvider' -import type { PipelineState, AuditTrailEntry, GateType } from '../domain/signalPipeline' -import type { ItemResponse, ListResponse } from './types' - -const provider = MockupSignalPipelineProvider - -export const signalPipelineService = { - async getPipelineState(signalId: string): Promise> { - const data = await provider.getPipelineState(signalId) - return { data } - }, - async evaluateGate(signalId: string, gateType: GateType): Promise> { - const data = await provider.evaluateGate(signalId, gateType) - return { data } - }, - async getAuditTrail(signalId: string): Promise> { - const data = await provider.getAuditTrail(signalId) - return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } - }, - async publishToFutureAvailability(signalId: string): Promise> { - const data = await provider.publishToFutureAvailability(signalId) - return { data } - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/sourceService.ts b/.claude/worktrees/agent-a82a3716/src/services/sourceService.ts deleted file mode 100644 index 0ae6788..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/sourceService.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { MockupDataSourceProvider } from '../provider/MockupDataSourceProvider' -import type { DataSource, ConnectorRun, SourceStatus, TermsStatus, SourceFilters } from '../domain/dataSource' -import type { ListResponse, ItemResponse } from './types' - -const provider = MockupDataSourceProvider - -export const sourceService = { - async getSources(filters?: SourceFilters): Promise> { - const data = await provider.getSources(filters) - return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } - }, - - async getSource(id: string): Promise> { - const data = await provider.getSource(id) - return { data } - }, - - async getConnectorRuns(sourceId: string): Promise> { - const data = await provider.getConnectorRuns(sourceId) - return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } } - }, - - async triggerMockRun(sourceId: string): Promise> { - const data = await provider.triggerMockRun(sourceId) - return { data } - }, - - async updateSourceStatus(id: string, status: SourceStatus): Promise> { - const data = await provider.updateSourceStatus(id, status) - return { data } - }, - - async markTermsStatus(id: string, termsStatus: TermsStatus): Promise> { - const data = await provider.markTermsStatus(id, termsStatus) - return { data } - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/services/types.ts b/.claude/worktrees/agent-a82a3716/src/services/types.ts deleted file mode 100644 index 3a9115c..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -// ── Error Codes ─────────────────────────────────────────────────────────────── - -export const ServiceErrorCode = { - NETWORK_ERROR: 'network_error', - UNAUTHORIZED: 'unauthorized', - FORBIDDEN: 'forbidden', - VALIDATION_ERROR: 'validation_error', - NOT_FOUND: 'not_found', - AI_GENERATION_FAILED: 'ai_generation_failed', - BACKEND_UNAVAILABLE: 'backend_unavailable', -} as const -export type ServiceErrorCode = typeof ServiceErrorCode[keyof typeof ServiceErrorCode] - -export interface ServiceError { - code: ServiceErrorCode - message: string - details?: unknown -} - -// ── Pagination ──────────────────────────────────────────────────────────────── - -export interface Pagination { - total: number - page: number - pageSize: number - hasMore: boolean -} - -/** @deprecated Use Pagination */ -export type ServiceMeta = Pagination - -// ── Response Shapes ─────────────────────────────────────────────────────────── - -export interface ServiceResponse { - data: T - meta?: Pagination - pagination?: Pagination - error?: ServiceError | string | null -} - -export type ListResponse = ServiceResponse -export type ItemResponse = ServiceResponse diff --git a/.claude/worktrees/agent-a82a3716/src/services/weightingService.ts b/.claude/worktrees/agent-a82a3716/src/services/weightingService.ts deleted file mode 100644 index 1b6ca24..0000000 --- a/.claude/worktrees/agent-a82a3716/src/services/weightingService.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { WeightingKey } from '../domain/needBuilder' - -type WeightProfile = Record - -const PROFILES: Record = { - OFFICE: { - area: 0.20, location: 0.25, budget: 0.20, timing: 0.15, - prestige: 0.10, accessibility: 0.05, expansionPotential: 0.03, flexibility: 0.02, - }, - LOGISTICS: { - area: 0.30, location: 0.20, budget: 0.20, timing: 0.15, - prestige: 0.02, accessibility: 0.08, expansionPotential: 0.03, flexibility: 0.02, - }, - RETAIL: { - area: 0.15, location: 0.30, budget: 0.20, timing: 0.10, - prestige: 0.12, accessibility: 0.08, expansionPotential: 0.03, flexibility: 0.02, - }, - PRODUCTION: { - area: 0.30, location: 0.20, budget: 0.20, timing: 0.15, - prestige: 0.02, accessibility: 0.07, expansionPotential: 0.04, flexibility: 0.02, - }, - DEFAULT: { - area: 0.25, location: 0.25, budget: 0.20, timing: 0.15, - prestige: 0.07, accessibility: 0.05, expansionPotential: 0.02, flexibility: 0.01, - }, -} - -export const weightingService = { - getDefaultWeights(assetType?: string): WeightProfile { - return { ...(PROFILES[assetType ?? 'DEFAULT'] ?? PROFILES.DEFAULT) } - }, -} diff --git a/.claude/worktrees/agent-a82a3716/src/stores/assistantStore.ts b/.claude/worktrees/agent-a82a3716/src/stores/assistantStore.ts deleted file mode 100644 index d33aa34..0000000 --- a/.claude/worktrees/agent-a82a3716/src/stores/assistantStore.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { create } from 'zustand' -import type { AssistantContext, AssistantMessage } from '../domain/assistant' - -interface AssistantState { - isOpen: boolean - context: AssistantContext | null - messages: AssistantMessage[] - isLoading: boolean - error: string | null - - open: () => void - close: () => void - setContext: (ctx: AssistantContext) => void - updateContext: (partial: Partial) => void - addMessage: (msg: AssistantMessage) => void - setLoading: (v: boolean) => void - setError: (e: string | null) => void - clearConversation: () => void -} - -export const useAssistantStore = create((set) => ({ - isOpen: false, - context: null, - messages: [], - isLoading: false, - error: null, - - open: () => set({ isOpen: true }), - close: () => set({ isOpen: false }), - setContext: (ctx) => set({ context: ctx }), - updateContext: (partial) => set((s) => ({ context: s.context ? { ...s.context, ...partial } : null })), - addMessage: (msg) => set((s) => ({ messages: [...s.messages, msg] })), - setLoading: (v) => set({ isLoading: v }), - setError: (e) => set({ error: e }), - clearConversation: () => set({ messages: [], error: null }), -})) diff --git a/.claude/worktrees/agent-a82a3716/src/stores/compareStore.ts b/.claude/worktrees/agent-a82a3716/src/stores/compareStore.ts deleted file mode 100644 index bcb46bf..0000000 --- a/.claude/worktrees/agent-a82a3716/src/stores/compareStore.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { create } from 'zustand' -import type { UnifiedMatchResult } from '../domain/unifiedResult' - -const MAX_COMPARE_ITEMS = 4 - -interface CompareState { - compareItems: UnifiedMatchResult[] - addToCompare: (result: UnifiedMatchResult) => void - removeFromCompare: (matchId: string) => void - clearCompare: () => void - isInCompare: (matchId: string) => boolean - isFull: () => boolean -} - -export const useCompareStore = create((set, get) => ({ - compareItems: [], - addToCompare: (result) => - set((state) => { - if (state.compareItems.length >= MAX_COMPARE_ITEMS) return state - if (state.compareItems.some(i => i.matchId === result.matchId)) return state - return { compareItems: [...state.compareItems, result] } - }), - removeFromCompare: (matchId) => - set((state) => ({ compareItems: state.compareItems.filter(i => i.matchId !== matchId) })), - clearCompare: () => set({ compareItems: [] }), - isInCompare: (matchId) => get().compareItems.some(i => i.matchId === matchId), - isFull: () => get().compareItems.length >= MAX_COMPARE_ITEMS, -})) diff --git a/.claude/worktrees/agent-a82a3716/src/stores/layoutStore.ts b/.claude/worktrees/agent-a82a3716/src/stores/layoutStore.ts deleted file mode 100644 index e3b8d55..0000000 --- a/.claude/worktrees/agent-a82a3716/src/stores/layoutStore.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { create } from 'zustand' -import { WorkspaceType } from '../domain/enums' - -export const RightPanelContentType = { - AI_CONTEXT: 'ai_context', - DETAIL_PREVIEW: 'detail_preview', - COMPARE_PREVIEW: 'compare_preview', - ACTIVITY_FEED: 'activity_feed', -} as const -export type RightPanelContentType = typeof RightPanelContentType[keyof typeof RightPanelContentType] - -interface LayoutState { - activeWorkspace: WorkspaceType - sidebarCollapsed: boolean - pinnedPanels: string[] - isRightPanelOpen: boolean - rightPanelContentType: RightPanelContentType | null - compareTrayVisible: boolean - selectedResultId: string | null - notificationsOpen: boolean - // Actions - setActiveWorkspace: (workspace: WorkspaceType) => void - toggleSidebar: () => void - pinPanel: (panelId: string) => void - unpinPanel: (panelId: string) => void - openRightPanel: (type: RightPanelContentType) => void - closeRightPanel: () => void - toggleRightPanel: (type: RightPanelContentType) => void - setCompareTrayVisible: (visible: boolean) => void - setSelectedResultId: (id: string | null) => void - toggleNotifications: () => void -} - -export const useLayoutStore = create((set, get) => ({ - activeWorkspace: WorkspaceType.SUPPLY, - sidebarCollapsed: false, - pinnedPanels: [], - isRightPanelOpen: false, - rightPanelContentType: null, - compareTrayVisible: false, - selectedResultId: null, - notificationsOpen: false, - - setActiveWorkspace: (workspace) => set({ activeWorkspace: workspace }), - toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })), - pinPanel: (panelId) => set((s) => ({ pinnedPanels: [...s.pinnedPanels, panelId] })), - unpinPanel: (panelId) => set((s) => ({ pinnedPanels: s.pinnedPanels.filter(id => id !== panelId) })), - openRightPanel: (type) => set({ isRightPanelOpen: true, rightPanelContentType: type }), - closeRightPanel: () => set({ isRightPanelOpen: false, rightPanelContentType: null }), - toggleRightPanel: (type) => { - const { isRightPanelOpen, rightPanelContentType } = get() - if (isRightPanelOpen && rightPanelContentType === type) { - set({ isRightPanelOpen: false, rightPanelContentType: null }) - } else { - set({ isRightPanelOpen: true, rightPanelContentType: type }) - } - }, - setCompareTrayVisible: (visible) => set({ compareTrayVisible: visible }), - setSelectedResultId: (id) => set({ selectedResultId: id }), - toggleNotifications: () => set((s) => ({ notificationsOpen: !s.notificationsOpen })), -})) diff --git a/.claude/worktrees/agent-a82a3716/src/stores/matchCenterStore.ts b/.claude/worktrees/agent-a82a3716/src/stores/matchCenterStore.ts deleted file mode 100644 index a9797f9..0000000 --- a/.claude/worktrees/agent-a82a3716/src/stores/matchCenterStore.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { create } from 'zustand' - -interface MatchCenterState { - selectedPropertyId: string | null - selectedNeedId: string | null - setSelectedProperty: (id: string | null) => void - setSelectedNeed: (id: string | null) => void - clearSelection: () => void -} - -export const useMatchCenterStore = create((set) => ({ - selectedPropertyId: null, - selectedNeedId: null, - setSelectedProperty: (id) => set({ selectedPropertyId: id }), - setSelectedNeed: (id) => set({ selectedNeedId: id }), - clearSelection: () => set({ selectedPropertyId: null, selectedNeedId: null }), -})) diff --git a/.claude/worktrees/agent-a82a3716/src/stores/sessionStore.ts b/.claude/worktrees/agent-a82a3716/src/stores/sessionStore.ts deleted file mode 100644 index 4cb9a32..0000000 --- a/.claude/worktrees/agent-a82a3716/src/stores/sessionStore.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { create } from 'zustand' -import { UserRole, WorkspaceType } from '../domain/enums' - -export interface MockUser { - id: string - email: string - name: string - role: UserRole - organizationId: string - organizationName: string - allowedWorkspaces: WorkspaceType[] -} - -export const SessionStatus = { - UNAUTHENTICATED: 'unauthenticated', - AUTHENTICATED: 'authenticated', - EXPIRED: 'expired', - RESTRICTED: 'restricted', - ONBOARDING: 'onboarding', -} as const -export type SessionStatus = typeof SessionStatus[keyof typeof SessionStatus] - -interface SessionState { - currentUser: MockUser | null - activeOrganizationId: string | null - isAuthenticated: boolean - sessionStatus: SessionStatus - login: (user: MockUser) => void - logout: () => void - switchOrganization: (organizationId: string) => void - setSessionStatus: (status: SessionStatus) => 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', - allowedWorkspaces: [WorkspaceType.SUPPLY, WorkspaceType.DEMAND, WorkspaceType.OPERATIONS], -} - -export const useSessionStore = create((set) => ({ - currentUser: mockUser, - activeOrganizationId: mockUser.organizationId, - isAuthenticated: true, - sessionStatus: SessionStatus.AUTHENTICATED, - login: (user) => set({ - currentUser: user, - activeOrganizationId: user.organizationId, - isAuthenticated: true, - sessionStatus: SessionStatus.AUTHENTICATED, - }), - logout: () => set({ - currentUser: null, - activeOrganizationId: null, - isAuthenticated: false, - sessionStatus: SessionStatus.UNAUTHENTICATED, - }), - switchOrganization: (organizationId) => set({ activeOrganizationId: organizationId }), - setSessionStatus: (status) => set({ sessionStatus: status }), -})) diff --git a/.claude/worktrees/agent-a82a3716/src/stores/shortlistStore.ts b/.claude/worktrees/agent-a82a3716/src/stores/shortlistStore.ts deleted file mode 100644 index 43c2ad4..0000000 --- a/.claude/worktrees/agent-a82a3716/src/stores/shortlistStore.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { create } from 'zustand' -import type { ShortlistItemInput } from '../domain/shortlist' - -interface ShortlistStore { - selectedShortlistId: string | null - dialogOpen: boolean - pendingItem: ShortlistItemInput | null - setSelectedShortlist: (id: string | null) => void - openAddDialog: (item: ShortlistItemInput) => void - closeAddDialog: () => void -} - -export const useShortlistStore = create((set) => ({ - selectedShortlistId: null, - dialogOpen: false, - pendingItem: null, - setSelectedShortlist: (id) => set({ selectedShortlistId: id }), - openAddDialog: (item) => set({ dialogOpen: true, pendingItem: item }), - closeAddDialog: () => set({ dialogOpen: false, pendingItem: null }), -})) diff --git a/.claude/worktrees/agent-a82a3716/src/stores/toastStore.ts b/.claude/worktrees/agent-a82a3716/src/stores/toastStore.ts deleted file mode 100644 index c825961..0000000 --- a/.claude/worktrees/agent-a82a3716/src/stores/toastStore.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { create } from 'zustand' - -export type ToastSeverity = 'success' | 'error' | 'warning' | 'info' - -export interface ToastMessage { - id: string - message: string - severity: ToastSeverity - duration?: number -} - -interface ToastState { - toasts: ToastMessage[] - showToast: (message: string, severity?: ToastSeverity, duration?: number) => void - dismissToast: (id: string) => void -} - -export const useToastStore = create((set) => ({ - toasts: [], - showToast: (message, severity = 'success', duration = 4000) => { - const id = crypto.randomUUID() - set((s) => ({ toasts: [...s.toasts, { id, message, severity, duration }] })) - }, - dismissToast: (id) => { - set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })) - }, -})) diff --git a/.claude/worktrees/agent-a82a3716/tsconfig.app.json b/.claude/worktrees/agent-a82a3716/tsconfig.app.json deleted file mode 100644 index 7f42e5f..0000000 --- a/.claude/worktrees/agent-a82a3716/tsconfig.app.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "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/.claude/worktrees/agent-a82a3716/tsconfig.json b/.claude/worktrees/agent-a82a3716/tsconfig.json deleted file mode 100644 index 1ffef60..0000000 --- a/.claude/worktrees/agent-a82a3716/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/.claude/worktrees/agent-a82a3716/tsconfig.node.json b/.claude/worktrees/agent-a82a3716/tsconfig.node.json deleted file mode 100644 index d3c52ea..0000000 --- a/.claude/worktrees/agent-a82a3716/tsconfig.node.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "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/.claude/worktrees/agent-a82a3716/vite.config.ts b/.claude/worktrees/agent-a82a3716/vite.config.ts deleted file mode 100644 index c676acd..0000000 --- a/.claude/worktrees/agent-a82a3716/vite.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import tailwindcss from '@tailwindcss/vite' - -export default defineConfig({ - plugins: [ - react(), - tailwindcss(), - ], -}) diff --git a/.gitignore b/.gitignore index e5b8580..2222c9d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ dist-ssr desktop.ini Thumbs.db +# Claude Code agent worktrees (transient copies of src/ — never commit) +.claude/worktrees/ + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/eslint.config.js b/eslint.config.js index 679e0be..c81f6f3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,7 +6,9 @@ import tseslint from 'typescript-eslint' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ - globalIgnores(['dist']), + // `.claude/worktrees/` holds transient agent copies of src/ — linting them + // produces thousands of duplicate parse errors and drowns out real findings. + globalIgnores(['dist', '.claude/**']), { files: ['**/*.{ts,tsx}'], extends: [