diff --git a/src/App.tsx b/src/App.tsx index 3984720..0f6ab45 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,9 +6,9 @@ import { ProtectedRoute } from './components/auth' import { WorkspaceType } from './domain/enums' import { useSessionStore } from './stores/sessionStore' +// Die Flächensuche ist entfernt — es bleibt ein einziger erreichbarer Einstieg. const WORKSPACE_HOME: Record = { [WorkspaceType.SUPPLY]: '/supply/dashboard', - [WorkspaceType.DEMAND]: '/demand/ai-search', } function RoleRedirect() { @@ -22,7 +22,6 @@ 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 Anfragencenter = lazy(() => import('./pages/supply/Anfragencenter')) const FutureAvailability = lazy(() => import('./pages/supply/FutureAvailability')) const DataQuality = lazy(() => import('./pages/supply/DataQuality')) const ReminderManager = lazy(() => import('./pages/supply/ReminderManager')) @@ -36,15 +35,6 @@ const Personalverwaltung = lazy(() => import('./pages/supply/Personalverwaltung' const Bearbeitungsverlauf = lazy(() => import('./pages/supply/Bearbeitungsverlauf')) const KanaeleSysteme = lazy(() => import('./pages/supply/KanaeleSysteme')) -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 Anfragen = lazy(() => import('./pages/demand/Anfragen')) -const Pipeline = lazy(() => import('./pages/demand/Pipeline')) -const PropertyDetail = lazy(() => import('./pages/demand/PropertyDetail')) - - function App() { return ( @@ -63,7 +53,6 @@ function App() { } /> } /> } /> - } /> } /> } /> } /> @@ -83,19 +72,6 @@ function App() { } /> } /> - - {/* Demand Workspace */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - diff --git a/src/assets/properties/map/prop-001.svg b/src/assets/properties/map/prop-001.svg new file mode 100644 index 0000000..776ce70 --- /dev/null +++ b/src/assets/properties/map/prop-001.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-002.svg b/src/assets/properties/map/prop-002.svg new file mode 100644 index 0000000..8d7100c --- /dev/null +++ b/src/assets/properties/map/prop-002.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-003.svg b/src/assets/properties/map/prop-003.svg new file mode 100644 index 0000000..0bb1e3f --- /dev/null +++ b/src/assets/properties/map/prop-003.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-004.svg b/src/assets/properties/map/prop-004.svg new file mode 100644 index 0000000..0fcdae7 --- /dev/null +++ b/src/assets/properties/map/prop-004.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-006.svg b/src/assets/properties/map/prop-006.svg new file mode 100644 index 0000000..54edc9f --- /dev/null +++ b/src/assets/properties/map/prop-006.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-007.svg b/src/assets/properties/map/prop-007.svg new file mode 100644 index 0000000..2c1d235 --- /dev/null +++ b/src/assets/properties/map/prop-007.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-008.svg b/src/assets/properties/map/prop-008.svg new file mode 100644 index 0000000..b8528f6 --- /dev/null +++ b/src/assets/properties/map/prop-008.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-009.svg b/src/assets/properties/map/prop-009.svg new file mode 100644 index 0000000..1fd0b6a --- /dev/null +++ b/src/assets/properties/map/prop-009.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-010.svg b/src/assets/properties/map/prop-010.svg new file mode 100644 index 0000000..680c110 --- /dev/null +++ b/src/assets/properties/map/prop-010.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-011.svg b/src/assets/properties/map/prop-011.svg new file mode 100644 index 0000000..6b684b3 --- /dev/null +++ b/src/assets/properties/map/prop-011.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-012.svg b/src/assets/properties/map/prop-012.svg new file mode 100644 index 0000000..801f058 --- /dev/null +++ b/src/assets/properties/map/prop-012.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-013.svg b/src/assets/properties/map/prop-013.svg new file mode 100644 index 0000000..771c153 --- /dev/null +++ b/src/assets/properties/map/prop-013.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-014.svg b/src/assets/properties/map/prop-014.svg new file mode 100644 index 0000000..8c46246 --- /dev/null +++ b/src/assets/properties/map/prop-014.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-015.svg b/src/assets/properties/map/prop-015.svg new file mode 100644 index 0000000..80e8a32 --- /dev/null +++ b/src/assets/properties/map/prop-015.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-016.svg b/src/assets/properties/map/prop-016.svg new file mode 100644 index 0000000..c349844 --- /dev/null +++ b/src/assets/properties/map/prop-016.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-017.svg b/src/assets/properties/map/prop-017.svg new file mode 100644 index 0000000..89e6b03 --- /dev/null +++ b/src/assets/properties/map/prop-017.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-018.svg b/src/assets/properties/map/prop-018.svg new file mode 100644 index 0000000..c028143 --- /dev/null +++ b/src/assets/properties/map/prop-018.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-019.svg b/src/assets/properties/map/prop-019.svg new file mode 100644 index 0000000..86ad610 --- /dev/null +++ b/src/assets/properties/map/prop-019.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-020.svg b/src/assets/properties/map/prop-020.svg new file mode 100644 index 0000000..a75a220 --- /dev/null +++ b/src/assets/properties/map/prop-020.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-021.svg b/src/assets/properties/map/prop-021.svg new file mode 100644 index 0000000..a5adbc6 --- /dev/null +++ b/src/assets/properties/map/prop-021.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-022.svg b/src/assets/properties/map/prop-022.svg new file mode 100644 index 0000000..4e060e8 --- /dev/null +++ b/src/assets/properties/map/prop-022.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-023.svg b/src/assets/properties/map/prop-023.svg new file mode 100644 index 0000000..a1e2e8d --- /dev/null +++ b/src/assets/properties/map/prop-023.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-024.svg b/src/assets/properties/map/prop-024.svg new file mode 100644 index 0000000..943f2a6 --- /dev/null +++ b/src/assets/properties/map/prop-024.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-025.svg b/src/assets/properties/map/prop-025.svg new file mode 100644 index 0000000..094f808 --- /dev/null +++ b/src/assets/properties/map/prop-025.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-027.svg b/src/assets/properties/map/prop-027.svg new file mode 100644 index 0000000..b9e6e38 --- /dev/null +++ b/src/assets/properties/map/prop-027.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-029.svg b/src/assets/properties/map/prop-029.svg new file mode 100644 index 0000000..42c2080 --- /dev/null +++ b/src/assets/properties/map/prop-029.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-031.svg b/src/assets/properties/map/prop-031.svg new file mode 100644 index 0000000..e3f3cc0 --- /dev/null +++ b/src/assets/properties/map/prop-031.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-032.svg b/src/assets/properties/map/prop-032.svg new file mode 100644 index 0000000..5be6119 --- /dev/null +++ b/src/assets/properties/map/prop-032.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-033.svg b/src/assets/properties/map/prop-033.svg new file mode 100644 index 0000000..efa88aa --- /dev/null +++ b/src/assets/properties/map/prop-033.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-034.svg b/src/assets/properties/map/prop-034.svg new file mode 100644 index 0000000..d01f6d5 --- /dev/null +++ b/src/assets/properties/map/prop-034.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-035.svg b/src/assets/properties/map/prop-035.svg new file mode 100644 index 0000000..1df61f3 --- /dev/null +++ b/src/assets/properties/map/prop-035.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-036.svg b/src/assets/properties/map/prop-036.svg new file mode 100644 index 0000000..e14cd66 --- /dev/null +++ b/src/assets/properties/map/prop-036.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-037.svg b/src/assets/properties/map/prop-037.svg new file mode 100644 index 0000000..ea97e5e --- /dev/null +++ b/src/assets/properties/map/prop-037.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-040.svg b/src/assets/properties/map/prop-040.svg new file mode 100644 index 0000000..bf53a66 --- /dev/null +++ b/src/assets/properties/map/prop-040.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-041.svg b/src/assets/properties/map/prop-041.svg new file mode 100644 index 0000000..dabb326 --- /dev/null +++ b/src/assets/properties/map/prop-041.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-042.svg b/src/assets/properties/map/prop-042.svg new file mode 100644 index 0000000..540ca4f --- /dev/null +++ b/src/assets/properties/map/prop-042.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-043.svg b/src/assets/properties/map/prop-043.svg new file mode 100644 index 0000000..8d326e9 --- /dev/null +++ b/src/assets/properties/map/prop-043.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-044.svg b/src/assets/properties/map/prop-044.svg new file mode 100644 index 0000000..70a7272 --- /dev/null +++ b/src/assets/properties/map/prop-044.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-045.svg b/src/assets/properties/map/prop-045.svg new file mode 100644 index 0000000..2a8890b --- /dev/null +++ b/src/assets/properties/map/prop-045.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-046.svg b/src/assets/properties/map/prop-046.svg new file mode 100644 index 0000000..eaba700 --- /dev/null +++ b/src/assets/properties/map/prop-046.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-047.svg b/src/assets/properties/map/prop-047.svg new file mode 100644 index 0000000..7bd52c3 --- /dev/null +++ b/src/assets/properties/map/prop-047.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-048.svg b/src/assets/properties/map/prop-048.svg new file mode 100644 index 0000000..a3617ca --- /dev/null +++ b/src/assets/properties/map/prop-048.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-050.svg b/src/assets/properties/map/prop-050.svg new file mode 100644 index 0000000..3b34586 --- /dev/null +++ b/src/assets/properties/map/prop-050.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-051.svg b/src/assets/properties/map/prop-051.svg new file mode 100644 index 0000000..456816e --- /dev/null +++ b/src/assets/properties/map/prop-051.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-052.svg b/src/assets/properties/map/prop-052.svg new file mode 100644 index 0000000..e3d1a08 --- /dev/null +++ b/src/assets/properties/map/prop-052.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-053.svg b/src/assets/properties/map/prop-053.svg new file mode 100644 index 0000000..d0c7eb8 --- /dev/null +++ b/src/assets/properties/map/prop-053.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-054.svg b/src/assets/properties/map/prop-054.svg new file mode 100644 index 0000000..1af578d --- /dev/null +++ b/src/assets/properties/map/prop-054.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-055.svg b/src/assets/properties/map/prop-055.svg new file mode 100644 index 0000000..1f101a6 --- /dev/null +++ b/src/assets/properties/map/prop-055.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-056.svg b/src/assets/properties/map/prop-056.svg new file mode 100644 index 0000000..11ae363 --- /dev/null +++ b/src/assets/properties/map/prop-056.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-057.svg b/src/assets/properties/map/prop-057.svg new file mode 100644 index 0000000..78a9f37 --- /dev/null +++ b/src/assets/properties/map/prop-057.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-058.svg b/src/assets/properties/map/prop-058.svg new file mode 100644 index 0000000..614feee --- /dev/null +++ b/src/assets/properties/map/prop-058.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-059.svg b/src/assets/properties/map/prop-059.svg new file mode 100644 index 0000000..7e1a0f6 --- /dev/null +++ b/src/assets/properties/map/prop-059.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-060.svg b/src/assets/properties/map/prop-060.svg new file mode 100644 index 0000000..ff78607 --- /dev/null +++ b/src/assets/properties/map/prop-060.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-061.svg b/src/assets/properties/map/prop-061.svg new file mode 100644 index 0000000..eb97061 --- /dev/null +++ b/src/assets/properties/map/prop-061.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-062.svg b/src/assets/properties/map/prop-062.svg new file mode 100644 index 0000000..c7f55ef --- /dev/null +++ b/src/assets/properties/map/prop-062.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-063.svg b/src/assets/properties/map/prop-063.svg new file mode 100644 index 0000000..78aee9b --- /dev/null +++ b/src/assets/properties/map/prop-063.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-064.svg b/src/assets/properties/map/prop-064.svg new file mode 100644 index 0000000..c20ae61 --- /dev/null +++ b/src/assets/properties/map/prop-064.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-065.svg b/src/assets/properties/map/prop-065.svg new file mode 100644 index 0000000..bf217ef --- /dev/null +++ b/src/assets/properties/map/prop-065.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-066.svg b/src/assets/properties/map/prop-066.svg new file mode 100644 index 0000000..b3fb0a9 --- /dev/null +++ b/src/assets/properties/map/prop-066.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-067.svg b/src/assets/properties/map/prop-067.svg new file mode 100644 index 0000000..d5212a6 --- /dev/null +++ b/src/assets/properties/map/prop-067.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-068.svg b/src/assets/properties/map/prop-068.svg new file mode 100644 index 0000000..c96cf3a --- /dev/null +++ b/src/assets/properties/map/prop-068.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-069.svg b/src/assets/properties/map/prop-069.svg new file mode 100644 index 0000000..1659e9b --- /dev/null +++ b/src/assets/properties/map/prop-069.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-070.svg b/src/assets/properties/map/prop-070.svg new file mode 100644 index 0000000..60304d3 --- /dev/null +++ b/src/assets/properties/map/prop-070.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-071.svg b/src/assets/properties/map/prop-071.svg new file mode 100644 index 0000000..011ee1b --- /dev/null +++ b/src/assets/properties/map/prop-071.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-072.svg b/src/assets/properties/map/prop-072.svg new file mode 100644 index 0000000..2f45dfd --- /dev/null +++ b/src/assets/properties/map/prop-072.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-073.svg b/src/assets/properties/map/prop-073.svg new file mode 100644 index 0000000..d4ba0b9 --- /dev/null +++ b/src/assets/properties/map/prop-073.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-074.svg b/src/assets/properties/map/prop-074.svg new file mode 100644 index 0000000..8cc1f71 --- /dev/null +++ b/src/assets/properties/map/prop-074.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-075.svg b/src/assets/properties/map/prop-075.svg new file mode 100644 index 0000000..23146ac --- /dev/null +++ b/src/assets/properties/map/prop-075.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-076.svg b/src/assets/properties/map/prop-076.svg new file mode 100644 index 0000000..73ef785 --- /dev/null +++ b/src/assets/properties/map/prop-076.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-077.svg b/src/assets/properties/map/prop-077.svg new file mode 100644 index 0000000..aa5d6a9 --- /dev/null +++ b/src/assets/properties/map/prop-077.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-078.svg b/src/assets/properties/map/prop-078.svg new file mode 100644 index 0000000..dc8d9dc --- /dev/null +++ b/src/assets/properties/map/prop-078.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-079.svg b/src/assets/properties/map/prop-079.svg new file mode 100644 index 0000000..4d2a15e --- /dev/null +++ b/src/assets/properties/map/prop-079.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-080.svg b/src/assets/properties/map/prop-080.svg new file mode 100644 index 0000000..5902651 --- /dev/null +++ b/src/assets/properties/map/prop-080.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-081.svg b/src/assets/properties/map/prop-081.svg new file mode 100644 index 0000000..b172340 --- /dev/null +++ b/src/assets/properties/map/prop-081.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-082.svg b/src/assets/properties/map/prop-082.svg new file mode 100644 index 0000000..34f1002 --- /dev/null +++ b/src/assets/properties/map/prop-082.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-083.svg b/src/assets/properties/map/prop-083.svg new file mode 100644 index 0000000..65bd79d --- /dev/null +++ b/src/assets/properties/map/prop-083.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-084.svg b/src/assets/properties/map/prop-084.svg new file mode 100644 index 0000000..b119900 --- /dev/null +++ b/src/assets/properties/map/prop-084.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-085.svg b/src/assets/properties/map/prop-085.svg new file mode 100644 index 0000000..0c23c5a --- /dev/null +++ b/src/assets/properties/map/prop-085.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-086.svg b/src/assets/properties/map/prop-086.svg new file mode 100644 index 0000000..6b8a8c9 --- /dev/null +++ b/src/assets/properties/map/prop-086.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-087.svg b/src/assets/properties/map/prop-087.svg new file mode 100644 index 0000000..51806f8 --- /dev/null +++ b/src/assets/properties/map/prop-087.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-088.svg b/src/assets/properties/map/prop-088.svg new file mode 100644 index 0000000..9a7649f --- /dev/null +++ b/src/assets/properties/map/prop-088.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-089.svg b/src/assets/properties/map/prop-089.svg new file mode 100644 index 0000000..04f4d98 --- /dev/null +++ b/src/assets/properties/map/prop-089.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-090.svg b/src/assets/properties/map/prop-090.svg new file mode 100644 index 0000000..0b8cf21 --- /dev/null +++ b/src/assets/properties/map/prop-090.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-091.svg b/src/assets/properties/map/prop-091.svg new file mode 100644 index 0000000..721539a --- /dev/null +++ b/src/assets/properties/map/prop-091.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-092.svg b/src/assets/properties/map/prop-092.svg new file mode 100644 index 0000000..fc50195 --- /dev/null +++ b/src/assets/properties/map/prop-092.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-093.svg b/src/assets/properties/map/prop-093.svg new file mode 100644 index 0000000..5208c6f --- /dev/null +++ b/src/assets/properties/map/prop-093.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-094.svg b/src/assets/properties/map/prop-094.svg new file mode 100644 index 0000000..4a3cdf4 --- /dev/null +++ b/src/assets/properties/map/prop-094.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-095.svg b/src/assets/properties/map/prop-095.svg new file mode 100644 index 0000000..697279d --- /dev/null +++ b/src/assets/properties/map/prop-095.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-096.svg b/src/assets/properties/map/prop-096.svg new file mode 100644 index 0000000..6c54573 --- /dev/null +++ b/src/assets/properties/map/prop-096.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-097.svg b/src/assets/properties/map/prop-097.svg new file mode 100644 index 0000000..1e979cb --- /dev/null +++ b/src/assets/properties/map/prop-097.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-098.svg b/src/assets/properties/map/prop-098.svg new file mode 100644 index 0000000..54ae7ec --- /dev/null +++ b/src/assets/properties/map/prop-098.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-099.svg b/src/assets/properties/map/prop-099.svg new file mode 100644 index 0000000..e4a2e54 --- /dev/null +++ b/src/assets/properties/map/prop-099.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-d01.svg b/src/assets/properties/map/prop-d01.svg new file mode 100644 index 0000000..0a5937a --- /dev/null +++ b/src/assets/properties/map/prop-d01.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-d02.svg b/src/assets/properties/map/prop-d02.svg new file mode 100644 index 0000000..8093bf9 --- /dev/null +++ b/src/assets/properties/map/prop-d02.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-d03.svg b/src/assets/properties/map/prop-d03.svg new file mode 100644 index 0000000..2afee54 --- /dev/null +++ b/src/assets/properties/map/prop-d03.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-d04.svg b/src/assets/properties/map/prop-d04.svg new file mode 100644 index 0000000..df41ddb --- /dev/null +++ b/src/assets/properties/map/prop-d04.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-d05.svg b/src/assets/properties/map/prop-d05.svg new file mode 100644 index 0000000..be7d14c --- /dev/null +++ b/src/assets/properties/map/prop-d05.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/map/prop-d06.svg b/src/assets/properties/map/prop-d06.svg new file mode 100644 index 0000000..bd9388f --- /dev/null +++ b/src/assets/properties/map/prop-d06.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-001.svg b/src/assets/properties/prop-001.svg new file mode 100644 index 0000000..81f1642 --- /dev/null +++ b/src/assets/properties/prop-001.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-002.svg b/src/assets/properties/prop-002.svg new file mode 100644 index 0000000..6974510 --- /dev/null +++ b/src/assets/properties/prop-002.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-003.svg b/src/assets/properties/prop-003.svg new file mode 100644 index 0000000..ea246a8 --- /dev/null +++ b/src/assets/properties/prop-003.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-004.svg b/src/assets/properties/prop-004.svg new file mode 100644 index 0000000..aca10dd --- /dev/null +++ b/src/assets/properties/prop-004.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-006.svg b/src/assets/properties/prop-006.svg new file mode 100644 index 0000000..3dec553 --- /dev/null +++ b/src/assets/properties/prop-006.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-007.svg b/src/assets/properties/prop-007.svg new file mode 100644 index 0000000..313f2e7 --- /dev/null +++ b/src/assets/properties/prop-007.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-008.svg b/src/assets/properties/prop-008.svg new file mode 100644 index 0000000..2a14b5d --- /dev/null +++ b/src/assets/properties/prop-008.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-009.svg b/src/assets/properties/prop-009.svg new file mode 100644 index 0000000..6ab7c81 --- /dev/null +++ b/src/assets/properties/prop-009.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-010.svg b/src/assets/properties/prop-010.svg new file mode 100644 index 0000000..6e2b6fe --- /dev/null +++ b/src/assets/properties/prop-010.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-011.svg b/src/assets/properties/prop-011.svg new file mode 100644 index 0000000..605fc80 --- /dev/null +++ b/src/assets/properties/prop-011.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-012.svg b/src/assets/properties/prop-012.svg new file mode 100644 index 0000000..22aeff6 --- /dev/null +++ b/src/assets/properties/prop-012.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-013.svg b/src/assets/properties/prop-013.svg new file mode 100644 index 0000000..d6034dc --- /dev/null +++ b/src/assets/properties/prop-013.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-014.svg b/src/assets/properties/prop-014.svg new file mode 100644 index 0000000..7fc0f9c --- /dev/null +++ b/src/assets/properties/prop-014.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-015.svg b/src/assets/properties/prop-015.svg new file mode 100644 index 0000000..f60f1ed --- /dev/null +++ b/src/assets/properties/prop-015.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-016.svg b/src/assets/properties/prop-016.svg new file mode 100644 index 0000000..cb3a627 --- /dev/null +++ b/src/assets/properties/prop-016.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-017.svg b/src/assets/properties/prop-017.svg new file mode 100644 index 0000000..0f1ff98 --- /dev/null +++ b/src/assets/properties/prop-017.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-018.svg b/src/assets/properties/prop-018.svg new file mode 100644 index 0000000..dad06ef --- /dev/null +++ b/src/assets/properties/prop-018.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-019.svg b/src/assets/properties/prop-019.svg new file mode 100644 index 0000000..0688f2d --- /dev/null +++ b/src/assets/properties/prop-019.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-020.svg b/src/assets/properties/prop-020.svg new file mode 100644 index 0000000..63ee0aa --- /dev/null +++ b/src/assets/properties/prop-020.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-021.svg b/src/assets/properties/prop-021.svg new file mode 100644 index 0000000..2199cd6 --- /dev/null +++ b/src/assets/properties/prop-021.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-022.svg b/src/assets/properties/prop-022.svg new file mode 100644 index 0000000..f975bc2 --- /dev/null +++ b/src/assets/properties/prop-022.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-023.svg b/src/assets/properties/prop-023.svg new file mode 100644 index 0000000..e0203d1 --- /dev/null +++ b/src/assets/properties/prop-023.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-024.svg b/src/assets/properties/prop-024.svg new file mode 100644 index 0000000..4226229 --- /dev/null +++ b/src/assets/properties/prop-024.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-025.svg b/src/assets/properties/prop-025.svg new file mode 100644 index 0000000..ea54275 --- /dev/null +++ b/src/assets/properties/prop-025.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-027.svg b/src/assets/properties/prop-027.svg new file mode 100644 index 0000000..d13c57e --- /dev/null +++ b/src/assets/properties/prop-027.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-029.svg b/src/assets/properties/prop-029.svg new file mode 100644 index 0000000..59765ad --- /dev/null +++ b/src/assets/properties/prop-029.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-031.svg b/src/assets/properties/prop-031.svg new file mode 100644 index 0000000..22b6db6 --- /dev/null +++ b/src/assets/properties/prop-031.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-032.svg b/src/assets/properties/prop-032.svg new file mode 100644 index 0000000..312300e --- /dev/null +++ b/src/assets/properties/prop-032.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-033.svg b/src/assets/properties/prop-033.svg new file mode 100644 index 0000000..236518d --- /dev/null +++ b/src/assets/properties/prop-033.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-034.svg b/src/assets/properties/prop-034.svg new file mode 100644 index 0000000..7f46387 --- /dev/null +++ b/src/assets/properties/prop-034.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-035.svg b/src/assets/properties/prop-035.svg new file mode 100644 index 0000000..7bcd1ef --- /dev/null +++ b/src/assets/properties/prop-035.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-036.svg b/src/assets/properties/prop-036.svg new file mode 100644 index 0000000..7763e1a --- /dev/null +++ b/src/assets/properties/prop-036.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-037.svg b/src/assets/properties/prop-037.svg new file mode 100644 index 0000000..3dffe30 --- /dev/null +++ b/src/assets/properties/prop-037.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-040.svg b/src/assets/properties/prop-040.svg new file mode 100644 index 0000000..cabeb58 --- /dev/null +++ b/src/assets/properties/prop-040.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-041.svg b/src/assets/properties/prop-041.svg new file mode 100644 index 0000000..7961ec0 --- /dev/null +++ b/src/assets/properties/prop-041.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-042.svg b/src/assets/properties/prop-042.svg new file mode 100644 index 0000000..003888b --- /dev/null +++ b/src/assets/properties/prop-042.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-043.svg b/src/assets/properties/prop-043.svg new file mode 100644 index 0000000..2e16d95 --- /dev/null +++ b/src/assets/properties/prop-043.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-044.svg b/src/assets/properties/prop-044.svg new file mode 100644 index 0000000..92b649c --- /dev/null +++ b/src/assets/properties/prop-044.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-045.svg b/src/assets/properties/prop-045.svg new file mode 100644 index 0000000..b2c959a --- /dev/null +++ b/src/assets/properties/prop-045.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-046.svg b/src/assets/properties/prop-046.svg new file mode 100644 index 0000000..f53a441 --- /dev/null +++ b/src/assets/properties/prop-046.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-047.svg b/src/assets/properties/prop-047.svg new file mode 100644 index 0000000..83b15ef --- /dev/null +++ b/src/assets/properties/prop-047.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-048.svg b/src/assets/properties/prop-048.svg new file mode 100644 index 0000000..94b0ae7 --- /dev/null +++ b/src/assets/properties/prop-048.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-050.svg b/src/assets/properties/prop-050.svg new file mode 100644 index 0000000..2004ebd --- /dev/null +++ b/src/assets/properties/prop-050.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-051.svg b/src/assets/properties/prop-051.svg new file mode 100644 index 0000000..61d6d64 --- /dev/null +++ b/src/assets/properties/prop-051.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-052.svg b/src/assets/properties/prop-052.svg new file mode 100644 index 0000000..1c047c3 --- /dev/null +++ b/src/assets/properties/prop-052.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-053.svg b/src/assets/properties/prop-053.svg new file mode 100644 index 0000000..bd75e60 --- /dev/null +++ b/src/assets/properties/prop-053.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-054.svg b/src/assets/properties/prop-054.svg new file mode 100644 index 0000000..931f637 --- /dev/null +++ b/src/assets/properties/prop-054.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-055.svg b/src/assets/properties/prop-055.svg new file mode 100644 index 0000000..28b7247 --- /dev/null +++ b/src/assets/properties/prop-055.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-056.svg b/src/assets/properties/prop-056.svg new file mode 100644 index 0000000..fc138ef --- /dev/null +++ b/src/assets/properties/prop-056.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-057.svg b/src/assets/properties/prop-057.svg new file mode 100644 index 0000000..22a0952 --- /dev/null +++ b/src/assets/properties/prop-057.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-058.svg b/src/assets/properties/prop-058.svg new file mode 100644 index 0000000..2474770 --- /dev/null +++ b/src/assets/properties/prop-058.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-059.svg b/src/assets/properties/prop-059.svg new file mode 100644 index 0000000..3286e0d --- /dev/null +++ b/src/assets/properties/prop-059.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-060.svg b/src/assets/properties/prop-060.svg new file mode 100644 index 0000000..d7e1725 --- /dev/null +++ b/src/assets/properties/prop-060.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-061.svg b/src/assets/properties/prop-061.svg new file mode 100644 index 0000000..9a99a41 --- /dev/null +++ b/src/assets/properties/prop-061.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-062.svg b/src/assets/properties/prop-062.svg new file mode 100644 index 0000000..261ac87 --- /dev/null +++ b/src/assets/properties/prop-062.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-063.svg b/src/assets/properties/prop-063.svg new file mode 100644 index 0000000..16d40a4 --- /dev/null +++ b/src/assets/properties/prop-063.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-064.svg b/src/assets/properties/prop-064.svg new file mode 100644 index 0000000..f57e6fa --- /dev/null +++ b/src/assets/properties/prop-064.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-065.svg b/src/assets/properties/prop-065.svg new file mode 100644 index 0000000..3416ef1 --- /dev/null +++ b/src/assets/properties/prop-065.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-066.svg b/src/assets/properties/prop-066.svg new file mode 100644 index 0000000..b95181a --- /dev/null +++ b/src/assets/properties/prop-066.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-067.svg b/src/assets/properties/prop-067.svg new file mode 100644 index 0000000..10d3c97 --- /dev/null +++ b/src/assets/properties/prop-067.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-068.svg b/src/assets/properties/prop-068.svg new file mode 100644 index 0000000..a03c55d --- /dev/null +++ b/src/assets/properties/prop-068.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-069.svg b/src/assets/properties/prop-069.svg new file mode 100644 index 0000000..e6e4f44 --- /dev/null +++ b/src/assets/properties/prop-069.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-070.svg b/src/assets/properties/prop-070.svg new file mode 100644 index 0000000..421bd1f --- /dev/null +++ b/src/assets/properties/prop-070.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-071.svg b/src/assets/properties/prop-071.svg new file mode 100644 index 0000000..fb7548d --- /dev/null +++ b/src/assets/properties/prop-071.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-072.svg b/src/assets/properties/prop-072.svg new file mode 100644 index 0000000..9b7c969 --- /dev/null +++ b/src/assets/properties/prop-072.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-073.svg b/src/assets/properties/prop-073.svg new file mode 100644 index 0000000..3e26f20 --- /dev/null +++ b/src/assets/properties/prop-073.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-074.svg b/src/assets/properties/prop-074.svg new file mode 100644 index 0000000..76a76df --- /dev/null +++ b/src/assets/properties/prop-074.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-075.svg b/src/assets/properties/prop-075.svg new file mode 100644 index 0000000..f4a618a --- /dev/null +++ b/src/assets/properties/prop-075.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-076.svg b/src/assets/properties/prop-076.svg new file mode 100644 index 0000000..deac971 --- /dev/null +++ b/src/assets/properties/prop-076.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-077.svg b/src/assets/properties/prop-077.svg new file mode 100644 index 0000000..ac92f0c --- /dev/null +++ b/src/assets/properties/prop-077.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-078.svg b/src/assets/properties/prop-078.svg new file mode 100644 index 0000000..9e9af5e --- /dev/null +++ b/src/assets/properties/prop-078.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-079.svg b/src/assets/properties/prop-079.svg new file mode 100644 index 0000000..97d9b06 --- /dev/null +++ b/src/assets/properties/prop-079.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-080.svg b/src/assets/properties/prop-080.svg new file mode 100644 index 0000000..53cff79 --- /dev/null +++ b/src/assets/properties/prop-080.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-081.svg b/src/assets/properties/prop-081.svg new file mode 100644 index 0000000..bea9070 --- /dev/null +++ b/src/assets/properties/prop-081.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-082.svg b/src/assets/properties/prop-082.svg new file mode 100644 index 0000000..1b96e4c --- /dev/null +++ b/src/assets/properties/prop-082.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-083.svg b/src/assets/properties/prop-083.svg new file mode 100644 index 0000000..1af6765 --- /dev/null +++ b/src/assets/properties/prop-083.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-084.svg b/src/assets/properties/prop-084.svg new file mode 100644 index 0000000..fc2fcb1 --- /dev/null +++ b/src/assets/properties/prop-084.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-085.svg b/src/assets/properties/prop-085.svg new file mode 100644 index 0000000..12f9465 --- /dev/null +++ b/src/assets/properties/prop-085.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-086.svg b/src/assets/properties/prop-086.svg new file mode 100644 index 0000000..a370bac --- /dev/null +++ b/src/assets/properties/prop-086.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-087.svg b/src/assets/properties/prop-087.svg new file mode 100644 index 0000000..67d03d5 --- /dev/null +++ b/src/assets/properties/prop-087.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-088.svg b/src/assets/properties/prop-088.svg new file mode 100644 index 0000000..bbee6e4 --- /dev/null +++ b/src/assets/properties/prop-088.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-089.svg b/src/assets/properties/prop-089.svg new file mode 100644 index 0000000..1e8a1f6 --- /dev/null +++ b/src/assets/properties/prop-089.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-090.svg b/src/assets/properties/prop-090.svg new file mode 100644 index 0000000..4a77439 --- /dev/null +++ b/src/assets/properties/prop-090.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-091.svg b/src/assets/properties/prop-091.svg new file mode 100644 index 0000000..9e31dcc --- /dev/null +++ b/src/assets/properties/prop-091.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-092.svg b/src/assets/properties/prop-092.svg new file mode 100644 index 0000000..0d16bcf --- /dev/null +++ b/src/assets/properties/prop-092.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-093.svg b/src/assets/properties/prop-093.svg new file mode 100644 index 0000000..b8b5826 --- /dev/null +++ b/src/assets/properties/prop-093.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-094.svg b/src/assets/properties/prop-094.svg new file mode 100644 index 0000000..c150908 --- /dev/null +++ b/src/assets/properties/prop-094.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-095.svg b/src/assets/properties/prop-095.svg new file mode 100644 index 0000000..982c584 --- /dev/null +++ b/src/assets/properties/prop-095.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-096.svg b/src/assets/properties/prop-096.svg new file mode 100644 index 0000000..23d4e6a --- /dev/null +++ b/src/assets/properties/prop-096.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-097.svg b/src/assets/properties/prop-097.svg new file mode 100644 index 0000000..36eb92c --- /dev/null +++ b/src/assets/properties/prop-097.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-098.svg b/src/assets/properties/prop-098.svg new file mode 100644 index 0000000..445283d --- /dev/null +++ b/src/assets/properties/prop-098.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-099.svg b/src/assets/properties/prop-099.svg new file mode 100644 index 0000000..ce55324 --- /dev/null +++ b/src/assets/properties/prop-099.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-d01.svg b/src/assets/properties/prop-d01.svg new file mode 100644 index 0000000..683229d --- /dev/null +++ b/src/assets/properties/prop-d01.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-d02.svg b/src/assets/properties/prop-d02.svg new file mode 100644 index 0000000..143413d --- /dev/null +++ b/src/assets/properties/prop-d02.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-d03.svg b/src/assets/properties/prop-d03.svg new file mode 100644 index 0000000..74e73fe --- /dev/null +++ b/src/assets/properties/prop-d03.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-d04.svg b/src/assets/properties/prop-d04.svg new file mode 100644 index 0000000..4db834d --- /dev/null +++ b/src/assets/properties/prop-d04.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-d05.svg b/src/assets/properties/prop-d05.svg new file mode 100644 index 0000000..9f3a6fd --- /dev/null +++ b/src/assets/properties/prop-d05.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/properties/prop-d06.svg b/src/assets/properties/prop-d06.svg new file mode 100644 index 0000000..a02c797 --- /dev/null +++ b/src/assets/properties/prop-d06.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/auth/OrganizationSwitcher.tsx b/src/components/auth/OrganizationSwitcher.tsx index 5d86dc8..7f22a23 100644 --- a/src/components/auth/OrganizationSwitcher.tsx +++ b/src/components/auth/OrganizationSwitcher.tsx @@ -4,7 +4,8 @@ import { useSwitchOrganization } from '../../hooks/useAuth' import { useSessionStore } from '../../stores/sessionStore' const MOCK_ORGANIZATIONS = [ - { id: 'org-wincasa', name: 'Wincasa AG' }, + // Technische Kennung bleibt unverändert — nur die sichtbare Bezeichnung wechselt. + { id: 'org-wincasa', name: 'Beispiel Immobilien AG' }, { id: 'org-mobimo', name: 'Mobimo Management AG' }, { id: 'org-ubs', name: 'UBS Asset Management RE' }, ] diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index af30d91..439d607 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -5,7 +5,6 @@ import { useLayoutStore } from '../../stores/layoutStore' import { useSessionStore } from '../../stores/sessionStore' import { WelcomeDialog } from '../onboarding/WelcomeDialog' import { RightContextPanel } from './RightContextPanel' -import { GlobalAIAssistantDrawer } from '../assistant' import { ToastProvider } from '../ui' import { Sidebar } from './AppShellSidebar' import { TopBar } from './AppShellTopBar' @@ -102,7 +101,6 @@ export function AppShell() { setMobileOpen(true)} @@ -115,7 +113,6 @@ export function AppShell() { - diff --git a/src/components/layout/AppShellTopBar.tsx b/src/components/layout/AppShellTopBar.tsx index f93f33a..27910f2 100644 --- a/src/components/layout/AppShellTopBar.tsx +++ b/src/components/layout/AppShellTopBar.tsx @@ -1,18 +1,14 @@ -import { Box, Typography, Chip, Button, IconButton } from '@mui/material' -import { Sparkles, Menu } from 'lucide-react' -import { WorkspaceType } from '../../domain/enums' -import { useAssistantStore } from '../../stores/assistantStore' +import { Box, IconButton } from '@mui/material' +import { Menu } from 'lucide-react' import { OrganizationContextBadge } from './OrganizationContextBadge' import { NotificationButton } from './NotificationButton' import { UserMenu } from './UserMenu' -import { WORKSPACE_CONFIG, getPageNameFromPath } from './appShellConfig' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface TopBarProps { - activeWorkspace: WorkspaceType pathname: string onMenuClick?: () => void isMobile?: boolean @@ -22,16 +18,15 @@ export interface TopBarProps { // Component // --------------------------------------------------------------------------- -export function TopBar({ activeWorkspace, pathname, onMenuClick, isMobile }: TopBarProps) { - const config = WORKSPACE_CONFIG[activeWorkspace] - const pageName = getPageNameFromPath(pathname) - const openAssistant = useAssistantStore(s => s.open) - - // Die Property-On-Seiten tragen ihren Titel bereits gross im Inhalt. Das Chip - // «Verwaltung» plus derselbe Seitenname darüber wäre eine doppelte Angabe und - // kostet nur vertikale Höhe. Andere Module behalten die Zeile unverändert. - const showBreadcrumb = !pathname.startsWith('/supply/team') - +/** + * Kopfzeile der Anwendung. + * + * Sie trägt nur noch, was global gilt: Mandant, Benachrichtigungen und Konto. + * Das Chip «Verwaltung» und der wiederholte Seitenname sind entfallen — beides + * stand nochmals gross im Inhaltsbereich und kostete nur Höhe. Ohne Trennlinie + * geht die Zeile ohne sichtbaren Bruch in den Seiteninhalt über. + */ +export function TopBar({ onMenuClick, isMobile }: TopBarProps) { return ( - {/* Left side */} + {/* Links steht nur noch der Menüaufruf auf schmalen Schirmen. */} {isMobile && ( )} - {showBreadcrumb && ( - <> - - - {pageName} - - - )} {/* Right side */} @@ -82,18 +54,6 @@ export function TopBar({ activeWorkspace, pathname, onMenuClick, isMobile }: Top - - - - diff --git a/src/components/layout/appShellConfig.ts b/src/components/layout/appShellConfig.ts index f6423f0..4b9b7e7 100644 --- a/src/components/layout/appShellConfig.ts +++ b/src/components/layout/appShellConfig.ts @@ -6,12 +6,8 @@ import { Building2, CheckSquare, Search, - List, - Columns2, ClipboardList, Radar, - MessageSquare, - Kanban, BellRing, Settings, Users, @@ -76,25 +72,24 @@ export const WORKSPACE_CONFIG: Record = { ], }, { path: '/supply/reminder-manager', label: 'Reminder Manager', icon: BellRing }, - { path: '/supply/anfragen', label: 'Anfragencenter', icon: MessageSquare }, { path: '/supply/data-quality', label: 'Datenpflege', icon: CheckSquare }, { path: '/supply/market-intelligence', label: 'Marktchancen', icon: Radar }, { path: '/supply/my-listings', label: 'Inserate', icon: ClipboardList }, ], }, + /** + * Die Flächensuche ist in diesem Branch entfernt. Der Eintrag bleibt nur + * bestehen, weil `WorkspaceType` als Domänen-Enum unangetastet bleibt und + * dieser Record vollständig sein muss — sichtbar ist er nirgends, denn + * `WORKSPACE_ORDER` führt ihn nicht mehr und es existiert keine Route. + */ [WorkspaceType.DEMAND]: { label: 'Suche', abbreviation: 'SU', icon: Search, - firstPath: '/demand/ai-search', + firstPath: '/supply/dashboard', 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/pipeline', label: 'Deal Pipeline', icon: Kanban }, - { path: '/demand/anfragen', label: 'Anfragen', icon: MessageSquare }, - ], + navItems: [], }, [WorkspaceType.OPERATIONS]: { label: 'Operations', @@ -112,7 +107,6 @@ export const WORKSPACE_CONFIG: Record = { // Ordered list for rendering workspace tabs export const WORKSPACE_ORDER: WorkspaceType[] = [ WorkspaceType.SUPPLY, - WorkspaceType.DEMAND, ] // --------------------------------------------------------------------------- @@ -121,7 +115,6 @@ export const WORKSPACE_ORDER: WorkspaceType[] = [ export function getWorkspaceFromPath(pathname: string): WorkspaceType | null { if (pathname.startsWith('/supply')) return WorkspaceType.SUPPLY - if (pathname.startsWith('/demand')) return WorkspaceType.DEMAND return null } @@ -139,10 +132,7 @@ export function getPageNameFromPath(pathname: string): string { // des Mitarbeiternamens bzw. «Erledigte auftraege». if (/^\/supply\/team\/personalverwaltung\/.+/.test(pathname)) return 'Personalblatt' if (/^\/supply\/team\/bearbeitungsverlauf\/.+/.test(pathname)) return 'Bearbeitungsverlauf' - if (/^\/demand\/results\/.+/.test(pathname)) return 'Match Detail' - if (/^\/demand\/property\//.test(pathname)) return 'Objekt Detail' if (/^\/supply\/properties\/.+/.test(pathname)) return 'Objekt Detail' - if (pathname === '/demand/anfragen') return 'Anfragen' const segment = pathname.split('/').filter(Boolean).pop() ?? '' return segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ') } diff --git a/src/components/match-center/MatchBriefingPanel.tsx b/src/components/match-center/MatchBriefingPanel.tsx index 80bd2d8..1ef6836 100644 --- a/src/components/match-center/MatchBriefingPanel.tsx +++ b/src/components/match-center/MatchBriefingPanel.tsx @@ -1,10 +1,8 @@ import { Box, CircularProgress, Typography } from '@mui/material' -import { useNavigate } from 'react-router' import { useMatchesByProperty, useApproveMatch } from '../../hooks/useMatches' import { usePropertyById } from '../../hooks/useProperties' import { useCreateReviewTask } from '../../hooks/useReviewQueue' 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' @@ -14,9 +12,7 @@ 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 createReviewTask = useCreateReviewTask() const showToast = useToastStore((s) => s.showToast) @@ -67,20 +63,8 @@ export function MatchBriefingPanel() { variant: 'secondary', onClick: () => { createReviewTask.mutate(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}`), - }, + // «Vergleichen» und «Details» führten in die Flächensuche. Die ist entfernt, + // also fallen die beiden Aktionen weg statt ins Leere zu zeigen. ] const vm = buildMatchCardViewModel(result, actions) diff --git a/src/components/supply/MatchabilityTabPanel.tsx b/src/components/supply/MatchabilityTabPanel.tsx index 97cdc9d..3af1968 100644 --- a/src/components/supply/MatchabilityTabPanel.tsx +++ b/src/components/supply/MatchabilityTabPanel.tsx @@ -9,9 +9,11 @@ export function MatchabilityTabPanel({ propertyId }: { propertyId: string }) { const setSelectedProperties = useOfferWizardStore(s => s.setSelectedProperties) const { data: matches = [], isLoading: loading } = useNeedMatchesForProperty(propertyId, { minScore: 80 }) + // Das Anfragencenter ist entfernt; die Auswahl führt neu ins Match Center, + // wo dasselbe Bedarfsprofil weiterbearbeitet wird. function handleNeedCardClick() { setSelectedProperties([propertyId]) - navigate('/supply/anfragen', { state: { tab: 1 } }) + navigate('/supply/match-center') } if (loading) { diff --git a/src/components/team/RotatableAgentWheel.tsx b/src/components/team/RotatableAgentWheel.tsx deleted file mode 100644 index 67dc7d7..0000000 --- a/src/components/team/RotatableAgentWheel.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { useCallback, useMemo, useRef, useState } from 'react' -import { Box, Typography } from '@mui/material' -import type { AgentDirectoryEntry } from '../../domain/agentDirectory' -import { AgentAvatar } from './AgentAvatar' -import { BADGE_COLORS, DS_BG, DS_BORDER, DS_TEXT } from '../../lib/ds' - -interface Props { - agents: AgentDirectoryEntry[] - selectedId: string | null - onSelect: (agentId: string) => void -} - -/** Grad Drehung je gezogenem Pixel. Bewusst träge — ein Wisch soll nicht durchs Team jagen. */ -const DRAG_DEGREES_PER_PX = 0.22 -/** Darunter gilt eine Bewegung als Zittern und wird verworfen. */ -const DRAG_THRESHOLD_PX = 10 -/** Bis zu dieser Zugweite wechselt höchstens ein Platz. */ -const SINGLE_STEP_LIMIT_PX = 90 -/** Durchmesser des gedachten Vollkreises; sichtbar ist nur die untere Hälfte. */ -const WHEEL_DIAMETER = 460 -const AVATAR_PX = 52 -const VISIBLE_HEIGHT = WHEEL_DIAMETER / 2 + 34 - -const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v)) - -/** - * Drehbares Agentenrad der Organigramm-Ansicht. - * - * Geometrie: `rotate(a) translateY(r)` schiebt ein Element bei a = 0 nach UNTEN. - * Ein Knoten mit Grundwinkel `i · step` steht also genau dann unten in der Mitte, - * wenn das Rad um `−i · step` gedreht ist. Genau das war der Fehler zuvor: mit - * `180 − i · step` landete der gewählte Mitarbeitende oben, das Dossier darunter - * gehörte folglich zum falschen Agenten. - * - * Der gedachte Vollkreis ragt nach oben aus dem Container heraus und wird von - * ihm abgeschnitten — es wird nichts am Bild zugeschnitten, nur beschnitten. - * - * Während des Ziehens läuft ausschliesslich ein lokaler Drehwert mit; die - * Auswahl wechselt erst beim Einrasten. Sonst würde bei jeder Mausbewegung das - * gesamte Dossier darunter neu aufgebaut. - */ -export function RotatableAgentWheel({ agents, selectedId, onSelect }: Props) { - const step = agents.length > 0 ? 360 / agents.length : 360 - const selectedIndex = Math.max(0, agents.findIndex(a => a.id === selectedId)) - - const [dragDeg, setDragDeg] = useState(0) - const [isDragging, setIsDragging] = useState(false) - const drag = useRef<{ startX: number } | null>(null) - - // Drehung, die den gewählten Mitarbeitenden nach unten in die Mitte bringt. - const baseDeg = -selectedIndex * step - const rotation = baseDeg + dragDeg - - const angles = useMemo(() => agents.map((_, i) => i * step), [agents, step]) - - /** Wie viele Plätze eine Zugweite bedeutet — kurze Züge höchstens einen. */ - const stepsFor = useCallback( - (dx: number) => { - if (Math.abs(dx) < DRAG_THRESHOLD_PX) return 0 - const raw = -(dx * DRAG_DEGREES_PER_PX) / step - const rounded = Math.round(raw) - if (Math.abs(dx) <= SINGLE_STEP_LIMIT_PX) return clamp(rounded, -1, 1) - return rounded - }, - [step], - ) - - const handlePointerDown = useCallback((e: React.PointerEvent) => { - drag.current = { startX: e.clientX } - setIsDragging(true) - e.currentTarget.setPointerCapture(e.pointerId) - }, []) - - const handlePointerMove = useCallback((e: React.PointerEvent) => { - if (!drag.current) return - setDragDeg((e.clientX - drag.current.startX) * DRAG_DEGREES_PER_PX) - }, []) - - const handlePointerUp = useCallback( - (e: React.PointerEvent) => { - if (!drag.current) return - const dx = e.clientX - drag.current.startX - drag.current = null - setIsDragging(false) - setDragDeg(0) - - const moved = stepsFor(dx) - if (moved === 0 || agents.length === 0) return - const next = agents[((selectedIndex + moved) % agents.length + agents.length) % agents.length] - if (next && next.id !== selectedId) onSelect(next.id) - }, - [stepsFor, agents, selectedIndex, selectedId, onSelect], - ) - - const move = useCallback( - (delta: number) => { - if (agents.length === 0) return - const next = agents[(selectedIndex + delta + agents.length) % agents.length] - if (next) onSelect(next.id) - }, - [agents, selectedIndex, onSelect], - ) - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); move(1) } - else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); move(-1) } - }, - [move], - ) - - const active = agents[selectedIndex] - - return ( - - - {agents.map((agent, index) => { - const angle = angles[index] - const isActive = index === selectedIndex - const size = isActive ? AVATAR_PX + 12 : AVATAR_PX - return ( - onSelect(agent.id)} - sx={{ - position: 'absolute', - left: '50%', - top: '50%', - // erst auf den Radius schieben, dann die Raddrehung ausgleichen, - // damit die Portraits aufrecht stehen - transform: `rotate(${angle}deg) translateY(${WHEEL_DIAMETER / 2 - AVATAR_PX - 6}px) rotate(${-angle - rotation}deg) translate(-50%, -50%)`, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: 0.4, - width: 132, - cursor: 'pointer', - opacity: isActive ? 1 : 0.72, - }} - > - - - {agent.name} - - {isActive && ( - - {agent.role} - - )} - - ) - })} - - - - Ziehen zum Wechseln - - - ) -} diff --git a/src/components/team/TeamLevelTabs.tsx b/src/components/team/TeamLevelTabs.tsx deleted file mode 100644 index 95ec22b..0000000 --- a/src/components/team/TeamLevelTabs.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useMemo } from 'react' -import { Box, Tab, Tabs, Typography } from '@mui/material' -import type { AgentLevel } from '../../domain/agentDirectory' -import { AGENT_LEVEL_ORDER } from '../../domain/agentDirectory' -import { agentDirectory } from '../../mock-data/agentDirectory' -import { AGENT_LEVEL_TAB_LABELS } from '../../lib/constants' -import { DS_TEXT } from '../../lib/ds' - -interface Props { - value: AgentLevel - onChange: (level: AgentLevel) => void -} - -/** - * Filterreiter über der Kreisdarstellung. - * - * Die Stufen sind kumulativ: «+ Zentrale Dienste» zeigt Kernteam UND Zentrale - * Dienste. Deshalb steht neben jedem Reiter die Gesamtzahl der dann sichtbaren - * Mitarbeitenden und nicht die Zahl der Stufe allein — sonst läse sich «+ GB - * Kommerziell · 7» so, als verschwänden die inneren Ringe. - * - * Bezeichnungen stammen wörtlich aus dem Konzeptdokument. Keine neuen - * Stufennamen erfunden. - */ -export function TeamLevelTabs({ value, onChange }: Props) { - const cumulativeCounts = useMemo(() => { - const counts = new Map() - let running = 0 - for (const level of AGENT_LEVEL_ORDER) { - running += agentDirectory.filter(a => a.level === level).length - counts.set(level, running) - } - return counts - }, []) - - return ( - onChange(next)} - variant="scrollable" - scrollButtons="auto" - allowScrollButtonsMobile - aria-label="Personalstufen" - sx={{ - minHeight: 40, - '& .MuiTab-root': { - textTransform: 'none', - fontWeight: 600, - fontSize: '0.875rem', - minHeight: 40, - py: 0.5, - }, - }} - > - {AGENT_LEVEL_ORDER.map((level) => ( - - {AGENT_LEVEL_TAB_LABELS[level] ?? level} - - {cumulativeCounts.get(level)} - - - } - /> - ))} - - ) -} diff --git a/src/components/team/TeamOverviewRing.tsx b/src/components/team/TeamOverviewRing.tsx index 8a46d68..e2f9964 100644 --- a/src/components/team/TeamOverviewRing.tsx +++ b/src/components/team/TeamOverviewRing.tsx @@ -93,6 +93,18 @@ export function TeamOverviewRing({ level }: Props) { return map }, []) + /** + * Nicht sichtbare Ringe werden nicht gezeichnet, statt nur durchsichtig zu + * sein. Durchsichtige Schaltflächen bleiben anspringbar — die Tabulatortaste + * liefe sonst durch Portraits, die niemand sieht. + */ + const visibleAgentCount = useMemo( + () => RING_SPECS + .slice(0, visibleUpTo + 1) + .reduce((sum, spec) => sum + (agentsByLevel.get(spec.level)?.length ?? 0), 0), + [agentsByLevel, visibleUpTo], + ) + const handleSelect = useCallback( (agent: AgentDirectoryEntry, element: HTMLElement | null) => { if (agent.isCore) { @@ -133,13 +145,12 @@ export function TeamOverviewRing({ level }: Props) { mx: 'auto', }} > - {specs.map((spec, index) => ( + {specs.slice(0, visibleUpTo + 1).map(spec => ( ))} @@ -186,7 +197,7 @@ export function TeamOverviewRing({ level }: Props) { mt: 0.25, }} > - {agentDirectory.length} + {visibleAgentCount} {showHubCaption && ( { - navigate(ANFRAGENCENTER_ROUTE, { state: { objectId } }) - }, [navigate, objectId]) - const firstMessage = item?.messageThread?.slice(0, 1) ?? [] return ( @@ -134,19 +126,12 @@ export function WorkItemDetailDrawer() { )} - {/* Nur die auslösende Nachricht — der Rest liegt im Anfragencenter. */} + {/* Nur die auslösende Nachricht. Der Verweis auf das Anfragencenter + ist entfallen — die Seite existiert in diesem Branch nicht mehr. */} {firstMessage.length > 0 && ( Auslösende Nachricht - )} diff --git a/src/components/team/__tests__/TeamOverviewRing.test.tsx b/src/components/team/__tests__/TeamOverviewRing.test.tsx index f575fae..107af2c 100644 --- a/src/components/team/__tests__/TeamOverviewRing.test.tsx +++ b/src/components/team/__tests__/TeamOverviewRing.test.tsx @@ -2,21 +2,22 @@ * Property On — Kreisdarstellung der Belegschaft. * * Abgesichert werden die Zusicherungen aus den Abnahmekriterien: die richtigen - * sieben im Kernteam, kumulative Stufenfilter, Portraits mit zugänglichem Namen - * und — fachlich am wichtigsten — dass ausschliesslich Kernteammitglieder in ein - * Dossier führen. Für die übrigen 29 gibt es keines, also darf auch kein Klick - * dorthin führen. + * sieben im Kernteam, Portraits mit zugänglichem Namen und — fachlich am + * wichtigsten — dass ausschliesslich Kernteammitglieder in ein Dossier führen. + * Für die übrigen 29 gibt es keines, also darf auch kein Klick dorthin führen. + * + * Die Stufenfilter sind entfallen: die Teamübersicht zeigt nur noch das + * Kernteam. Die Komponente beherrscht die äusseren Ringe technisch weiterhin, + * die Seite ruft sie aber fest mit `CORE` auf. */ import { describe, it, expect } from 'vitest' import { useLocation } from 'react-router' -import { screen, fireEvent, within } from '@testing-library/react' +import { screen, fireEvent } from '@testing-library/react' import { renderWithProviders } from '../../../test/teamTestUtils' import { TeamOverviewRing } from '../TeamOverviewRing' -import { TeamLevelTabs } from '../TeamLevelTabs' import { agentDirectory } from '../../../mock-data/agentDirectory' import { AgentLevel } from '../../../domain/agentDirectory' -import { AGENT_LEVEL_TAB_LABELS } from '../../../lib/constants' function LocationProbe() { const location = useLocation() @@ -46,33 +47,6 @@ describe('Kernteam', () => { }) }) -describe('TeamLevelTabs', () => { - it('zeigt die vier Stufen mit dem Wortlaut des Konzepts', () => { - renderWithProviders( {}} />) - for (const level of Object.values(AgentLevel)) { - expect(screen.getByText(AGENT_LEVEL_TAB_LABELS[level])).toBeInTheDocument() - } - }) - - it('beziffert die Stufen kumulativ, nicht je Stufe einzeln', () => { - renderWithProviders( {}} />) - const tabs = screen.getAllByRole('tab') - expect(within(tabs[0]).getByText('7')).toBeInTheDocument() - expect(within(tabs[1]).getByText('25')).toBeInTheDocument() - expect(within(tabs[2]).getByText('32')).toBeInTheDocument() - expect(within(tabs[3]).getByText('36')).toBeInTheDocument() - }) - - it('meldet die gewählte Stufe nach oben', () => { - const seen: string[] = [] - renderWithProviders( - seen.push(l)} />, - ) - fireEvent.click(screen.getByText(AGENT_LEVEL_TAB_LABELS[AgentLevel.COMMERCIAL])) - expect(seen).toEqual([AgentLevel.COMMERCIAL]) - }) -}) - describe('TeamOverviewRing', () => { it('gibt jedem Portrait einen zugänglichen Namen aus Name und Funktion', () => { renderWithProviders() @@ -90,9 +64,11 @@ describe('TeamOverviewRing', () => { expect(labels).toHaveLength(agentDirectory.length) }) - it('zeigt die Gesamtzahl in der Nabe', () => { + it('nennt in der Nabe die Zahl der tatsächlich gezeigten Mitarbeitenden', () => { + // Nicht die Gesamtzahl 36: sichtbar sind nur die sieben des Kernteams, und + // die Nabe darf nicht mehr behaupten, als das Bild hergibt. renderWithProviders() - expect(screen.getByText(String(agentDirectory.length))).toBeInTheDocument() + expect(screen.getByText(String(core.length))).toBeInTheDocument() expect(screen.getByText('digitale Mitarbeitende')).toBeInTheDocument() }) @@ -127,10 +103,11 @@ describe('TeamOverviewRing', () => { ).toBeInTheDocument() }) - it('blendet äussere Ringe aus, solange nur das Kernteam gewählt ist', () => { - const { container } = renderWithProviders() - const hidden = container.querySelectorAll('[aria-hidden="true"]') - // Drei der vier Ringe sind ausgeblendet; die Führungslinien zählen zusätzlich. - expect(hidden.length).toBeGreaterThanOrEqual(3) + it('zeichnet die äusseren Ringe gar nicht erst, wenn nur das Kernteam gilt', () => { + // Früher waren sie nur durchsichtig — und damit weiterhin mit der + // Tabulatortaste erreichbar. Unsichtbare, aber anspringbare Schaltflächen + // sind eine Falle, deshalb werden sie nicht mehr gerendert. + renderWithProviders() + expect(screen.getAllByRole('button')).toHaveLength(core.length) }) }) diff --git a/src/components/team/index.ts b/src/components/team/index.ts index b56c69b..a6e1d27 100644 --- a/src/components/team/index.ts +++ b/src/components/team/index.ts @@ -29,7 +29,6 @@ export { TeamSectionHeader } from './TeamSectionHeader' export { TeamKpiSection } from './TeamKpiSection' // Kreisdarstellung der Belegschaft — ersetzt das frühere Kernteam-Kartenraster -export { TeamLevelTabs } from './TeamLevelTabs' export { TeamOverviewRing } from './TeamOverviewRing' export { AgentRing } from './AgentRing' export { AgentRingNode } from './AgentRingNode' @@ -46,7 +45,6 @@ export { } from './AgentDossier' export type { DossierSegment } from './AgentDossier' export { AgentDescriptionTab } from './AgentDescriptionTab' -export { RotatableAgentWheel } from './RotatableAgentWheel' export { AgentListPanel } from './AgentListPanel' export { AgentDossierHeader } from './AgentDossierHeader' export { AgentInfoBox } from './AgentInfoBox' diff --git a/src/lib/propertyImageResolver.ts b/src/lib/propertyImageResolver.ts index cfde0ee..bd8ff80 100644 --- a/src/lib/propertyImageResolver.ts +++ b/src/lib/propertyImageResolver.ts @@ -1,104 +1,49 @@ import { AssetType } from '../domain/enums' -// ── Image pools (curated Unsplash IDs, all w=800&h=400&fit=crop) ────────────── -// Rule: ALL images are interior shots — no exteriors, no city skylines, no landscapes. +/** + * Objektbilder — ausschliesslich lokale Assets. + * + * Früher zeigten die Objekte auf Unsplash. Das hiess: ohne Netz kein Bild, und + * 98 Objekte teilten sich 29 Fotos, verschiedene Adressen trugen also dasselbe + * Bild. Beides ist behoben — je Objekt-ID liegt genau eine Datei unter + * `assets/properties/`. + * + * Die Zuordnung läuft über die stabile Objekt-ID, nicht über den sichtbaren + * Titel: Titel ändern sich, IDs nicht. + * + * Die Dateien sind erzeugte Vektorgrafiken und keine Fotografien. Werden echte + * Aufnahmen geliefert, ersetzt man sie unter demselben Dateinamen — am Code + * ändert sich dann nichts. + */ -// prettier-ignore -const OFFICE_IDS = [ - 'photo-1497366858526-0766c4080517', // premium reception & lobby - 'photo-1497366811353-6870744d04b2', // floor-to-ceiling glass, elegant interior - 'photo-1497366216548-37526070297c', // clean open-plan office - 'photo-1454165804606-c3d57bc86b40', // corporate meeting / workspace - 'photo-1568992687947-868a62a9f521', // contemporary office with plants - 'photo-1572025442646-866d16c84a54', // bright modern office - 'photo-1553028826-f4804a6dba3b', // open workspace, natural light - 'photo-1534536281715-e28d76689b4d', // converted warehouse loft - 'photo-1556761175-5973dc0f32e7', // industrial co-working, exposed concrete - 'photo-1541746972996-4e0b0f43e02a', // creative loft workspace - 'photo-1515187029135-18ee286d815b', // brick + open ceiling office - 'photo-1504384308090-c894fdcc538d', // industrial-chic open space -] +type UrlMap = Record -// Rotate so each sub-pool maps the same sequential index to a different photo -const rotate = (arr: T[], n: number): T[] => [...arr.slice(n), ...arr.slice(0, n)] +const OBJECT_FILES = import.meta.glob('../assets/properties/*.svg', { + eager: true, + query: '?url', + import: 'default', +}) as UrlMap -const POOL = { - office_premium: rotate(OFFICE_IDS, 0), - office_modern: rotate(OFFICE_IDS, 4), - office_industrial: rotate(OFFICE_IDS, 8), - logistics: [ - 'photo-1586528116311-ad8dd3c8310d', - 'photo-1553413077-190dd305871c', - 'photo-1587293852726-70cfa4d5f99f', - 'photo-1566932769119-25a49e6b5c6d', - 'photo-1474396651759-b49538a26a2d', - ], - retail: [ - 'photo-1441986300917-64674bd600d8', - 'photo-1567958451986-2de427a4a0be', - 'photo-1555529669-e69e7aa0ba9a', - 'photo-1604719312566-8912e9227c6a', - 'photo-1555396273-367ea4eb4db5', - 'photo-1472851294608-062f824d29cc', - 'photo-1483985988355-763728e1802b', - 'photo-1445205170230-053b83016050', - ], - gastro: [ - 'photo-1414235077428-338989a2e8c0', - 'photo-1517248135467-4c7edcad34c4', - 'photo-1544148103-0773bf10d330', - 'photo-1559339352-11d035aa65ce', - 'photo-1466978913421-dad2ebd01d17', - 'photo-1521017432531-fbd92d768814', - ], - light_industrial: [ - 'photo-1565515636339-5de8c80eba38', - 'photo-1581091226825-a6a2a5aee158', - 'photo-1571008887538-b36bb32f4571', - 'photo-1504307651254-35680f356dfd', - ], -} +const MAP_FILES = import.meta.glob('../assets/properties/map/*.svg', { + eager: true, + query: '?url', + import: 'default', +}) as UrlMap -const BASE = 'https://images.unsplash.com/' -const PARAMS = '?w=800&h=400&fit=crop' - -// Sequential pick — guaranteed unique until pool wraps (used at init time in mock data) -function pickAt(pool: string[], index: number): string { - return `${BASE}${pool[index % pool.length]}${PARAMS}` -} - -// ── District sets (exported so properties.ts can replicate the pool-key logic) ── - -export const PREMIUM_DISTRICTS = new Set([ - 'innenstadt', 'kreis 1', 'altstadt', 'seefeld', 'kreis 8', - 'city', 'zürich city', 'bern innenstadt', 'zug innenstadt', -]) - -export const INDUSTRIAL_DISTRICTS = new Set([ - 'zürich-west', 'zürich west', 'west', 'binz', 'zürich-binz', - 'altstetten', 'hürlimann', 'technopark', 'industrial', 'industriezone', -]) - -// ── Pool-key helper (exported so properties.ts can count per pool) ───────────── - -export type PoolKey = keyof typeof POOL - -export function getPoolKey(assetType: string | undefined, district: string, prestige: number): PoolKey { - switch (assetType) { - case AssetType.LOGISTICS: return 'logistics' - case AssetType.RETAIL: return 'retail' - case AssetType.GASTRO: return 'gastro' - case AssetType.LIGHT_INDUSTRIAL: - case AssetType.PRODUCTION: return 'light_industrial' - default: { - if (prestige >= 78 || PREMIUM_DISTRICTS.has(district)) return 'office_premium' - if (INDUSTRIAL_DISTRICTS.has(district)) return 'office_industrial' - return 'office_modern' - } +/** Pfad → Objekt-ID, damit die Suche nicht vom Ablageort abhängt. */ +function byPropertyId(files: UrlMap): UrlMap { + const out: UrlMap = {} + for (const [path, url] of Object.entries(files)) { + const name = path.split('/').pop()?.replace(/\.svg$/, '') + if (name) out[name] = url } + return out } -// ── Main resolver ───────────────────────────────────────────────────────────── +const OBJECT_IMAGES = byPropertyId(OBJECT_FILES) +const MAP_IMAGES = byPropertyId(MAP_FILES) + +// ── Auflösung ───────────────────────────────────────────────────────────────── export interface ImageResolverParams { assetType?: string @@ -107,24 +52,27 @@ export interface ImageResolverParams { prestige?: number floorLevel?: number propertyId?: string - poolIndex?: number // sequential index within pool — set by properties.ts forEach for duplicate-free assignment } -export function resolvePropertyImage(p: ImageResolverParams): string { - const district = (p.district ?? '').toLowerCase() - const prestige = p.prestige ?? 60 - const key = getPoolKey(p.assetType, district, prestige) - const pool = POOL[key] - // Sequential index (set at mock-data init) guarantees no duplicates within a pool - if (p.poolIndex !== undefined) return pickAt(pool, p.poolIndex) - // Fallback: hash-based (used when poolIndex is unavailable, e.g. runtime calls) - let h = 0 - const seed = p.propertyId ?? `${p.city}${p.district}${p.assetType}` - for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) >>> 0 - return pickAt(pool, h) +/** + * Bild eines Objekts. Ohne hinterlegte Datei wird nichts erfunden — der Aufrufer + * entscheidet dann selbst, was er zeigt. + */ +export function resolvePropertyImage(p: ImageResolverParams): string | undefined { + return p.propertyId ? OBJECT_IMAGES[p.propertyId] : undefined } -// ── Image overlay label ─────────────────────────────────────────────────────── +/** Schematischer Lageplan eines Objekts. */ +export function resolvePropertyMapImage(propertyId: string): string | undefined { + return MAP_IMAGES[propertyId] +} + +/** Wie viele Objektbilder tatsächlich hinterlegt sind — für Prüfzwecke. */ +export function countPropertyImages(): number { + return Object.keys(OBJECT_IMAGES).length +} + +// ── Bildbeschriftung ────────────────────────────────────────────────────────── const ASSET_LABELS: Partial> = { [AssetType.OFFICE]: 'Bürofläche', diff --git a/src/mock-data/properties.ts b/src/mock-data/properties.ts index 2d13390..aa881fc 100644 --- a/src/mock-data/properties.ts +++ b/src/mock-data/properties.ts @@ -1,6 +1,6 @@ import { AssetType, ResultType, AvailabilityStatus, DataFreshness, RiskLevel } from '../domain/enums' import type { Property } from '../domain/property' -import { resolvePropertyImage, getPoolKey } from '../lib/propertyImageResolver' +import { resolvePropertyImage, resolvePropertyMapImage } from '../lib/propertyImageResolver' export const mockProperties: Property[] = [ @@ -43,8 +43,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 5.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-001', description: 'Moderne Bürofläche im aufstrebenden Stadtquartier Zürich-West, direkt beim Trendviertel Freilager. Die hellen, offen gestalteten Flächen bieten optimale Bedingungen für kollaboratives Arbeiten. Grosszügige Fensterfronten sorgen für viel Tageslicht. Das Gebäude verfügt über einen repräsentativen Empfangsbereich, Sitzungsräume sowie eine Gemeinschaftsterrasse mit Blick auf die Stadt. ÖV-Anbindung in unmittelbarer Nähe (Tram 4/13, S-Bahn Hardbrücke, 4 Minuten zu Fuss).', @@ -101,8 +99,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 36, ancillaryCosts: 3.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BS-2022-002', description: 'Grossflächige Logistikanlage in direkter Rheinnähe, Kleinhüningen Basel. Zwei separate Lagerhallen A und B mit je eigenem Tor und Rampenanlage. Sprinkleranlage und Hallentemperierung vorhanden. Sehr gute Erschliessung via A2/A3, 30 Lastwagenstellplätze auf dem Areal. Ausbaupotenzial von 800 m² auf dem Grundstück verfügbar.', units: [ @@ -159,8 +155,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 5.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1454165804606-c3d57bc86b40?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2021-007', description: 'Helle Büroflächen im 2. Obergeschoss an der Thurgauerstrasse, Zürich-Oerlikon. Drei Einheiten — zwei belegt und für Pre-Market freigegeben, eine flexible Einheit ab 150 m² direkt verfügbar. Hervorragende ÖV-Anbindung via Tram 11 und S-Bahn Oerlikon. Break-out-Option auf September 2026, danach gesamte 720 m² frei.', units: [ @@ -218,8 +212,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 4.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1568992687947-868a62a9f521?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BS-2021-008', description: 'Moderne Büroflächen im renommierten Dreispitz-Areal, Hochbergerstrasse Basel. Vier unabhängige Etagen für verschiedene Teams oder Mieter. Parkhaus im Gebäude vorhanden, hervorragende Anbindung an Tram 11. Derzeit vollvermietet an Pharma Research GmbH — alle Einheiten für Pre-Market freigegeben.', units: [ @@ -276,8 +268,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 2.8, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1553413077-190dd305871c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2022-009', description: 'Modernes Logistikzentrum im Töss-Quartier Winterthur, mit direkter A1-Anbindung. Lager Nord und Süd können separat oder gemeinsam angemietet werden. Ebene Andienung mit 4 Toren, Hallenhöhe 8 m, Bodenbelastung 3 t/m². 25 Lastwagenstellplätze. Ausbaupotenzial von 600 m² vorhanden.', units: [ @@ -332,8 +322,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 8.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-010', description: 'Exklusive Retailfläche am Löwenplatz, Zürich-Innenstadt. Direkter Zugang zu Bahnhof und Tramknotenpunkt, maximale Laufkundschaft rund um die Uhr. EG-Verkaufsfläche mit repräsentativer Schaufensterfront. Derzeit von Fashion Concept GmbH belegt — Pre-Market-Freigabe bereits aktiv.', units: [ @@ -388,8 +376,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 2.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1565043589221-1a6fd9ae45c7?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BE-2023-011', description: 'Weiträumige Produktions- und Logistikhalle im Gewerbequartier Brünnen Bern. Kranbahn (5 t) in Halle West, Dreiphasenstrom 400V vorhanden. Gut angebunden an A12-Anschluss Bern-Bümpliz. Zwei Hallenabschnitte separat oder gemeinsam anmietbar. Ausbaupotenzial von 1200 m² auf dem Grundstück möglich.', units: [ @@ -445,8 +431,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 36, ancillaryCosts: 6.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1504384308090-c894fdcc538d?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZG-2022-012', description: 'Repräsentative Büroetage im Stadtturm Zug, Industriestrasse 2. Panoramablick auf die Zuger Innenstadt und den See vom 5. Obergeschoss. Moderne Ausstattung mit Klimaanlage und Unterflurverkabelung. Ideal für Finanzdienstleister, Family Offices und internationale Unternehmen. Break-out-Option auf August 2026.', units: [ @@ -504,8 +488,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 5.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1486325212027-8081e485255e?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2021-013', description: 'Gemischte Gewerbe- und Bürofläche im Altstetten Park, direkt an der Badenerstrasse Zürich. Kombination aus EG-Laden und zwei Büroetagen — ideal für Firmen mit Showroom-Bedarf. Gut erschlossen via ÖV (Tram, Bus) und Autobahn. Flexible Einheiten ab 100 m² verfügbar.', units: [ @@ -562,8 +544,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 2.8, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1504917595217-d4dc5ebe6122?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BL-2022-014', description: 'Dreiteilige Logistikhalle im Industriepark Pratteln, unmittelbar an der A2-Anschlussstelle. Lager A und B für Palettenlagerung ausgelegt, Bürobereich EG ideal als Kombi-Nutzung (ab 200 m² flexibel). Ausbaupotenzial von 1500 m² auf dem Grundstück. Break-out-Option auf Juli 2026.', importedFrom: 'SAP RE-FX', @@ -624,8 +604,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 6.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1567538096630-e0c55bd6374c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-046', description: 'Vollausgebautes Ladenlokal an der Birmensdorferstrasse, Zürich Kreis 3 (Wiedikon). Schaufensterfront EG, Rückraum (20 m²) als Werkstatt oder Lager nutzbar. 3-Zimmer-Wohnung im gleichen Gebäude per Oktober 2026 mitbezugsbereit. Quartierstrasse mit stabiler Passantenfrequenz, ÖV 5 Min. (Tram 9/14). Verfügbar Q4/2026.', units: [ @@ -680,8 +658,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 6.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1472851294608-062f824d29cc?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-047', description: 'Bezugsfertiges Ladenlokal in Zürich Kreis 8 (Riesbach/Seefeld), Seefeldstrasse. Grosszügige Schaufensterfront, EG, vollausgebaut. Hinterraum (22 m²) ideal für Werkstatt oder Lager. Gehobenes Quartier mit hohem Kundenpotenzial, nahe Zürichsee. 2 Parkplätze, Veloinfrastruktur, ÖV 4 Min. Verfügbar November 2026.', units: [ @@ -736,8 +712,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 5.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'WAE-2024-048', description: 'Vollausgebautes Ladenlokal im Zentrum von Wädenswil, direkt an der Seestrasse. Schaufensterfront EG, Werkstattraum im Hinterhaus (25 m²). 4 Parkplätze im Innenhof. Bahnhof Wädenswil (S-Bahn) in 8 Min. zu Fuss — ideale Lage für Fahrradhandel und -service in der Seegemeinde. Verfügbar Q4/2026.', units: [ @@ -792,8 +766,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 6.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-040', description: 'Bezugsfertiges Ladenlokal an der Langstrasse, Zürich Kreis 5. Grosszügige Schaufensterfront, EG, vollständig ausgebaut. Nebenraum (28 m²) als Werkstatt oder Lager nutzbar. Lebhafter Standort mit hohem Fussgängeraufkommen. Verfügbar November 2026. Kein Parkplatz direkt, Velounterstand vorhanden.', units: [ @@ -846,8 +818,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 6.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-041', description: 'Vollausgebautes Ladenlokal am Helvetiaplatz, Zürich Kreis 4. EG mit Schaufensterfront, Hinterraum für Werkstatt oder Lager. Kleines Lager im UG optional. Standort mit guter Passantenfrequenz, 2 Parkplätze. Verfügbar Februar 2027 — ideal für Bezug Q1/27.', units: [ @@ -901,8 +871,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 84, ancillaryCosts: 3.8, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2023-042', description: 'Loft-Bürofläche mit Industrie-Charakter, Pfingstweidstrasse 6, Zürich-West. 162 m², 1. OG in einem ehemaligen Fabrikgebäude — hohe Decken (3.2 m), Sichtbeton, natürliches Licht. Raum für 10–12 Arbeitsplätze + 2 Sitzungszimmer möglich. 4 Parkplätze im Hof, Gemeinschaftskantine im EG. Umfeld: dynamisches Startup-Ökosystem, junge Unternehmen und Kreativagenturen im Haus. Mietvertrag 7 Jahre bevorzugt, Erweiterungsoption 90 m² verfügbar.', units: [ @@ -955,8 +923,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 9.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2022-043', description: 'Exklusive Büroetage an der Bahnhofstrasse, Zürich Kreis 1 — eine der prestigiösesten Adressen der Schweiz. 515 m² im 6. Obergeschoss mit Panoramablick über die Innenstadt. PREMIUM-Ausbau, Klimaanlage, grosszügige Sitzungszimmer-Suite, repräsentativer Empfangsbereich und Concierge-Service. Ideal für Finanzdienstleister, Family Offices, Vermögensverwalter. Mietvertrag min. 10 Jahre, ÖV-Anbindung 2 Min. (Tram, S-Bahn).', units: [ @@ -1007,8 +973,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 8.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1486325212027-8081e485255e?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2022-044', description: 'Repräsentative Bürofläche im Seefeldquartier, nahe Bellevue und Zürichsee. 495 m² im 5. OG — helle Räume mit Seesicht, grosszügige Fensterflächen, Tageslicht von drei Seiten. PREMIUM-Ausbau mit Klimaanlage, 5 Sitzungszimmer, Empfangszone und Teeküche. 8 Tiefgaragenplätze. ÖV 3 Min. (Tram 2, 4). Mietvertrag mind. 10 Jahre — ideal für Wealth Management und professionelle Dienstleister.', units: [ @@ -1060,8 +1024,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 84, ancillaryCosts: 4.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2023-045', description: 'Kompakte Bürofläche (175 m²) im Industrie-Groove von Zürich-West, Limmatstrasse. Platz für 10 Arbeitsplätze und 2 Besprechungsräume. Einfacher Ausbau mit hohem Tageslichtanteil, lebhaftes Startup-Umfeld mit Gemeinschaftskantine und Co-Working-Angebot im selben Gebäude. 4 Parkplätze im Innenhof. ÖV-Anbindung 7 Minuten zu Fuss. Mietvertrag bevorzugt 7 Jahre.', units: [ @@ -1114,8 +1076,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 6.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BE-EXT-003', description: 'Attraktive Ladenfläche an der Bahnhofstrasse Bern, direkt beim Hauptbahnhof. EG, sehr hohe Laufkundschaft, maximale Visibilität. Ideal für bekannte Handelsmarken oder Dienstleister mit direktem Kundenkontakt.', hardFacts: { fitOut: 'FULL' }, @@ -1152,8 +1112,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 5.5, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1498049794561-7780e7231661?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-EXT-004', description: 'Grosszügige Gewerbefläche an der Europaallee, Zürich Kreis 4, direkt beim Hauptbahnhof. Modernes Gebäude mit flexibler Aufteilung. Hervorragende ÖV-Anbindung und erstklassige Lage. Geeignet als Büro, Showroom oder gemischte Nutzung.', hardFacts: { fitOut: 'FULL' }, @@ -1197,8 +1155,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 5.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1556761175-b413da4baf72?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'LU-MW-015', description: 'Helle Bürofläche am Kasernenplatz Luzern, im Herzen der Innenstadt. Das 3. Obergeschoss bietet eine grosszügige, flexible Raumaufteilung mit natürlichem Licht von drei Seiten. ÖV-Verbindungen in unmittelbarer Nähe (Bahnhof Luzern, 5 Minuten zu Fuss). Ideal für professionelle Dienstleister.', hardFacts: { fitOut: 'BASIC' }, @@ -1241,8 +1197,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 3.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1525498128493-380d1990a112?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BL-MW-016', description: 'Grosszügige Lagerhalle an der Rheinfelderstrasse, direkt im Industriegebiet Muttenz. Ebenerdige Anlieferung mit breitem Tor, 35 Aussenparkplätze. Anbindung A2/A3 unter 5 Minuten. Ideal für Lagerung, Distribution und Leichtindustrie.', hardFacts: { fitOut: 'SHELL' }, @@ -1286,8 +1240,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 7.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1556742502-ec7c0e9f34b6?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-EXT-017', description: 'Erstklassige Ladenfläche an der Löwenstrasse im Zürcher Hauptbahnhof-Umfeld. EG, direkt an der Fussgängerzone mit sehr hoher Frequenz durch Pendler und Touristen. Schaufensterfront auf zwei Seiten.', hardFacts: { fitOut: 'FULL' }, @@ -1330,8 +1282,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 4.5, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BE-EXT-018', description: 'Praktische Bürofläche im 2. Obergeschoss, Breitenrainstrasse Bern. Grosszügige, lichtdurchflutete Räume in ruhigem Quartier nahe dem Stadtzentrum. Bahn- und Busanbindung in 8 Minuten Fussweg. Ideal für Büros, Beratungsfirmen oder Praxen.', hardFacts: { fitOut: 'BASIC' }, @@ -1369,8 +1319,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 2.8, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1572021335469-31706a17aaef?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BS-MW-019', description: 'Grosszügige Produktionshalle mit erhöhten Bodenlasten und Dreiphasenstrom im Industriequartier Kleinhüningen Basel. Direkte LKW-Zufahrt über die Voltastrasse. Ideal für Leichtindustrie, Montage oder Lagerhaltung.', hardFacts: { fitOut: 'SHELL' }, @@ -1413,8 +1361,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 5.5, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1524758631624-e2822e304c36?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZG-MW-020', description: 'Grosszügige Bürofläche im 4. Obergeschoss, Industriestrasse Zug. Ruhige Lage mit Panorama-Bergblick, direkter Autobahnanschluss A4. Ideal für Firmen, die von der Zuger Steuerpolitik profitieren möchten, ohne Premium-Innenstadtmieten zu zahlen.', hardFacts: { fitOut: 'BASIC' }, @@ -1458,8 +1404,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 6.5, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1528698827591-e19ccd7bc23d?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'GE-MW-021', description: 'Exklusive Verkaufsfläche im Erdgeschoss an der Rue du Rhône, Genf Zentrum. Eine der prestigeträchtigsten Einkaufsstrassen der Schweiz. Ideal für Luxusmarken, Juweliere oder hochwertige Dienstleister mit Anforderung an Prestige und Visibilität.', hardFacts: { fitOut: 'PREMIUM' }, @@ -1502,8 +1446,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 4.5, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'SG-EXT-022', description: 'Helle Bürofläche an der Marktgasse im Zentrum St. Gallens. Das 2. Obergeschoss bietet eine zusammenhängende, gut aufteilbare Fläche in historischem Stadtquartier. St. Gallen HB in 6 Minuten zu Fuss. Ideal für Kanzleien, Dienstleister oder regionale Niederlassungen.', hardFacts: { fitOut: 'BASIC' }, @@ -1706,8 +1648,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 4.5, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1542314831-068cd1dbfeeb?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-EXT-031', description: 'Repräsentative Bürofläche im 3. Obergeschoss im modernen Hardturm-Areal Zürich-West. Die zusammenhängende Fläche von 780 m² ist offen gestaltet und kann flexibel unterteilt werden. Grosse Fensterflächen, Klimaanlage und ein Untergeschoss-Parkhaus sind vorhanden. Das Gebäude befindet sich in zentraler Lage mit hervorragender Anbindung an den öffentlichen Verkehr (Tram 4/13, Bahnhof Hardbrücke, 4 Minuten zu Fuss). Übergabe ab Oktober 2025 möglich.', hardFacts: { fitOut: 'FULL' }, @@ -1745,8 +1685,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 5.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1613545325278-f24b0cae1224?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-MW-032', description: 'Stilvolles Büroloft im 5. Obergeschoss eines ehemaligen Industriegebäudes an der Pfingstweidstrasse, Kreis 5. Die offene Loftstruktur mit Sichtbetondecken und -wänden schafft ein inspirierendes Arbeitsumfeld. Raumhöhe ca. 3,5 m, Holzböden, individuelle Klimatisierung. Panoramablick über die Dächer Zürichs. Ideal für kreative Unternehmen und Tech-Firmen. Verfügbar ab November 2025.', hardFacts: { fitOut: 'BASIC' }, @@ -1784,8 +1722,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 7.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1528698827591-e19ccd7bc23d?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BE-MW-033', description: 'Attraktive Retailfläche an der Marktgasse, direkt in der Berner Fussgängerzone. EG-Fläche mit hoher Laufkundschaft durch Tourismus und Pendler. Tram und Bus direkt vor dem Haus. Ideal für Fashion, Lifestyle oder Gastronomie-Konzepte.', hardFacts: { fitOut: 'FULL' }, @@ -1823,8 +1759,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 6.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1534398079543-7ae6d016b86a?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BE-EXT-034', description: 'Ansprechende Ladenfläche im trendigen Quartier Lorraine, Bern. EG mit Schaufensterfront, gut positioniert für Kreativwirtschaft und gehobene Kundschaft. Busanbindung in 6 Minuten zum Bahnhof Bern.', hardFacts: { fitOut: 'BASIC' }, @@ -1888,8 +1822,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 2.5, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1590239926044-4131a46e3f27?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BS-EXT-036', description: 'Freistehendes Lager-/Logistikgebäude direkt an der Klybeckstrasse, Basel Hafen. Ebenerdige Anlieferung, 38 Aussenparkplätze. Hervorragende Lage für Distribution in der Nordwestschweiz. Kran und Sprinkleranlage vorhanden.', hardFacts: { fitOut: 'SHELL' }, @@ -1935,8 +1867,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 4.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BE-2024-037', description: 'Erstklassige Retailfläche an der Spitalgasse, direkt im Herzstück der Berner Innenstadt. Direkter Anschluss an Bahnhof und Tramknotenpunkt — maximale Erreichbarkeit. EG-Verkaufsfläche mit grosser Schaufensterfront. Derzeit von Modehaus Bern AG belegt, Pre-Market-Freigabe bereits aktiv.', units: [ @@ -1996,8 +1926,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 6.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-050', description: 'Vollausgebautes Ladenlokal an der Militärstrasse, Zürich Kreis 5. Grosse Schaufensterfront, Erdgeschoss, hohe Passantenfrequenz. Verfügbar Oktober 2026.', units: [ @@ -2049,8 +1977,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 5.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-051', description: 'Ladenlokal an der Gotthardstrasse, Thalwil — Seegemeinde mit guter S-Bahn-Anbindung. EG mit Schaufensterfront, vollständig ausgebaut. Verfügbar November 2026.', units: [ @@ -2102,8 +2028,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 5.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1528698827591-e19ccd7bc23d?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-052', description: 'Vollausgebautes Ladenlokal an der Bahnhofstrasse Uster. EG, Schaufensterfront, gute Passantenfrequenz nahe Bahnhof. Verfügbar Oktober 2026.', units: [ @@ -2155,8 +2079,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 6.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-053', description: 'Ladenlokal an der Langstrasse, Zürich Kreis 4. Belebter Standort mit hoher Fussgängerfrequenz, grosser Schaufensterfront. Vollständig ausgebaut, EG. Verfügbar Januar 2027.', units: [ @@ -2208,8 +2130,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 7.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-054', description: 'Exklusives Ladenlokal im Seefeld-Quartier, Zürich Kreis 8. EG, Schaufensterfront, gehobene Lage. Idealer Standort für Lifestyle- und Premiummarken. Verfügbar November 2026.', units: [ @@ -2262,8 +2182,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 6.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1534398079543-7ae6d016b86a?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-EXT-055', description: 'Ladenfläche an der Weststrasse, Zürich Kreis 3. EG mit Schaufensterfront, gut ausgebaut. Mittlere bis hohe Passantenfrequenz, Tramanbindung direkt.', units: [ @@ -2310,8 +2228,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 5.5, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-EXT-056', description: 'Gewerbefläche an der Hohlstrasse, Zürich Kreis 4. 1. OG, kein Schaufenster — Near-Miss für Retail-Anfragen mit EG-Anforderung. Basisausbau.', units: [ @@ -2358,8 +2274,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 6.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1528698827591-e19ccd7bc23d?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-MW-057', description: 'Ladenlokal an der Winterthurerstrasse, Zürich Kreis 6. EG, Schaufensterfront, mittlere Passantenfrequenz. Vollausbau gemäss Inserat.', units: [ @@ -2405,8 +2319,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 6.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-058', description: 'Ladenlokal an der Grubenstrasse, Zürich Kreis 3. EG, Schaufensterfront, mittlere bis hohe Passantenfrequenz. Vollständig ausgebaut. Verfügbar Oktober 2026.', units: [ @@ -2457,8 +2369,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 6.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-059', description: 'Ladenlokal an der Quellenstrasse, Zürich Kreis 5. Derzeit von Textilhandel GmbH belegt — Auszug per November 2026 bekannt. Pre-Market-Freigabe aktiv. EG, Schaufensterfront, hohe Passantenfrequenz.', units: [ @@ -2513,8 +2423,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 84, ancillaryCosts: 3.8, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2023-060', description: 'Bürofläche im 2. OG an der Hardturmstrasse, Zürich-West. 180 m², Basisausbau, 5 Parkplätze im Hof. Dynamisches Industrie-Umfeld, gute ÖV-Anbindung. Verfügbar Oktober 2025.', units: [ @@ -2565,8 +2473,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 84, ancillaryCosts: 3.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2023-061', description: 'Bürofläche im 1. OG, Herdernstrasse Kreis 5. 165 m², Basisausbau, 3 Parkplätze. Lebhaftes Umfeld, kurze Wege zu ÖV. Verfügbar Januar 2026.', units: [ @@ -2617,8 +2523,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 84, ancillaryCosts: 4.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1524758631624-e2822e304c36?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2023-062', description: 'Bürofläche im 3. OG, Militärstrasse 36, Zürich Kreis 4. 195 m², Basisausbau, 4 Parkplätze. Gute Erreichbarkeit per Tram und S-Bahn. Verfügbar April 2026.', units: [ @@ -2669,8 +2573,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 84, ancillaryCosts: 4.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2023-063', description: 'Büroloft im ehemaligen Schiffbau-Komplex, Zürich-West. 170 m², hohe Decken (3.4 m), Industriecharakter, Startup- und Community-Atmosphäre. Keine eigenen Parkplätze, hervorragende ÖV-Anbindung. Verfügbar Januar 2026.', units: [ @@ -2722,8 +2624,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 4.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-EXT-064', description: 'Bürofläche im 2. OG, Förrlibuckstrasse, Zürich-West. 185 m², Basisausbau, 4 Parkplätze. Gutes Preis-Leistungs-Verhältnis für das Umfeld.', units: [ @@ -2769,8 +2669,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 3.5, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1524758631624-e2822e304c36?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-EXT-065', description: 'Bürofläche im 4. OG, Binzmühlestrasse Oerlikon. 175 m², Basisausbau, günstiger Mietpreis — jedoch ausserhalb des Wunschperimeters Zürich-West/Kreis 4–5.', units: [ @@ -2815,8 +2713,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 84, ancillaryCosts: 4.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2023-066', description: 'Bürofläche im Technopark, Escher-Wyss-Platz, Zürich Kreis 5. 148 m² im 3. OG, Basisausbau, exzellenter Talent-Zugang im Tech-Umfeld. 3 Parkplätze, direkte Tramanbindung. Verfügbar Juli 2026.', units: [ @@ -2868,8 +2764,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 3.5, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-EXT-067', description: 'Bürofläche im 1. OG, Altstetterstrasse. 190 m², Basisausbau, günstiger Mietpreis. Ausserhalb des gewünschten Distrikts, aber niedrige Kosten als Kompromissoption.', units: [ @@ -2914,8 +2808,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 84, ancillaryCosts: 4.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2023-068', description: 'Bürofläche im Viadukt-Quartier, Limmatstrasse 264, Zürich-West. 160 m² im 2. OG, Basisausbau mit Industriecharme. 2 Parkplätze, Tram vor dem Haus. Verfügbar Dezember 2025.', units: [ @@ -2965,8 +2857,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 84, ancillaryCosts: 4.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-069', description: 'Bürofläche Pfingstweidstrasse 20, Zürich-West. Derzeit von Kreativagentur GmbH belegt — Auszug Oktober 2026 bekannt. Pre-Market-Freigabe aktiv. 172 m², 2. OG, Basisausbau.', units: [ @@ -3021,8 +2911,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 9.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2022-070', description: 'Exklusive Büroetage an der Talstrasse, Zürich Kreis 1. 505 m² im 4. OG, PREMIUM-Ausbau, Klimaanlage. Repräsentativer Eingangsbereich, Panoramablick. Idealer Standort für Finanzdienstleister und Family Offices. Verfügbar Juli 2026.', units: [ @@ -3073,8 +2961,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 9.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2022-071', description: 'Prestigiöse Büroetage am Bleicherweg, Zürich Kreis 1. 485 m² im 5. OG, PREMIUM-Ausbau, Klimaanlage, Seeblick. Erstklassige Adresse für Vermögensverwalter und Finanzinstitute. Verfügbar April 2026.', units: [ @@ -3125,8 +3011,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 9.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2022-072', description: 'Exklusive Büroetage am Utoquai, Zürich Seefeld. 525 m² im 3. OG mit direktem Seeblick, PREMIUM-Ausbau, Klimaanlage. Toplage für internationale Gesellschaften und Wealth-Management-Firmen. Verfügbar Oktober 2026.', units: [ @@ -3177,8 +3061,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 9.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2022-073', description: 'Repräsentative Büroetage an der Rämistrasse, Zürich Seefeld. 468 m² im 2. OG, PREMIUM-Ausbau mit Klimaanlage. Ruhige, elegante Umgebung nahe Zürichsee. Verfügbar Januar 2026.', units: [ @@ -3230,8 +3112,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 9.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-EXT-074', description: 'Premium-Bürofläche an der Uraniastrasse, Zürich Kreis 1. 510 m², 4. OG, Klimaanlage, PREMIUM-Ausbau. Sehr gute Adresse nahe Bahnhofstrasse.', units: [ @@ -3278,8 +3158,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 9.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-EXT-075', description: 'Premium-Bürofläche an der Seefeldstrasse, Zürich Seefeld. 540 m², 3. OG, PREMIUM-Ausbau. Klimaanlage gemäss Inserat, Bestätigung ausstehend.', units: [ @@ -3324,8 +3202,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 9.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2022-076', description: 'Exklusive Penthouse-Büroetage an der Stockerstrasse, Zürich Kreis 1. 495 m² im 6. OG, PREMIUM-Ausbau, Klimaanlage, Terrasse. Ideale Adresse für Wealth-Management und Private Equity. Verfügbar April 2026.', units: [ @@ -3375,8 +3251,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 9.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-077', description: 'Bürofläche Löwenstrasse 16, Zürich Kreis 1. Derzeit von Private Equity GmbH belegt — Auszug September 2026 bekannt. Pre-Market-Freigabe aktiv. 515 m², 4. OG, PREMIUM-Ausbau, Klimaanlage.', units: [ @@ -3432,8 +3306,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 7.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BE-2024-078', description: 'Erstklassige Retailfläche in der Kramgasse, Bern Altstadt. EG mit Schaufensterfront, hohe Fussgängerfrequenz in der historischen Einkaufsmeile. Vollständig ausgebaut. Verfügbar Juli 2026.', units: [ @@ -3485,8 +3357,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 7.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BE-2024-079', description: 'Grosszügige Retailfläche an der Marktgasse, Bern Innenstadt. 310 m² im EG, Schaufensterfront, vollständig ausgebaut. Starke Passantenfrequenz, Tram direkt vor dem Haus. Verfügbar Oktober 2026.', units: [ @@ -3539,8 +3409,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 7.5, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1528698827591-e19ccd7bc23d?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BE-EXT-080', description: 'Ladenfläche an der Spitalgasse, Bern Innenstadt. EG, Schaufensterfront, hohe Passantenfrequenz. Vollausbau gemäss Inserat.', units: [ @@ -3587,8 +3455,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 7.5, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1534398079543-7ae6d016b86a?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BE-EXT-081', description: 'Ladenfläche an der Münstergasse, Bern Innenstadt. EG, grosse Schaufensterfront, historische Lage nahe dem Berner Münster. Vollausbau gemäss Inserat.', units: [ @@ -3634,8 +3500,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 5.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BE-2024-082', description: 'Ladenfläche im Quartier Länggasse, Bern. EG, kein Schaufenster, Basisausbau — Near-Miss für Innenstadt-Suche. Ruhigeres Wohnquartier ausserhalb des Zentrums. Verfügbar Juli 2026.', units: [ @@ -3688,8 +3552,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 7.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1528698827591-e19ccd7bc23d?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BE-MW-083', description: 'Retailfläche an der Neuengasse, Bern Marktgasse-Umfeld. EG, Schaufensterfront, hohe Passantenfrequenz. Vollausbau gemäss Maison Work Inserat.', units: [ @@ -3736,8 +3598,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 2.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1590239926044-4131a46e3f27?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BS-2024-084', description: 'Moderne Logistikhalle in Basel Kleinhüningen. 2200 m², 4 Andocktore, Deckenhöhe 12,5 m, ebenerdige Zufahrt. Ideale Lage für Nordwestschweiz-Distribution, Rheinanschluss in der Nähe. Verfügbar Oktober 2025.', units: [ @@ -3788,8 +3648,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 2.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1590239926044-4131a46e3f27?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BS-2024-085', description: 'Logistikhalle in Basel St. Johann. 2500 m², 3 Andocktore, Deckenhöhe 12 m. Gute Autobahnanbindung A2/A3. Verfügbar Januar 2026.', units: [ @@ -3840,8 +3698,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 2.2, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1590239926044-4131a46e3f27?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BL-2024-086', description: 'Grosse Logistikhalle in der Industriezone Muttenz. 2800 m², 3 Andocktore, Deckenhöhe 12 m, direkte LKW-Zufahrt. Günstigste Option im Grossraum Basel. Verfügbar April 2026.', units: [ @@ -3893,8 +3749,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 2.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1590239926044-4131a46e3f27?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BL-EXT-087', description: 'Grosse Lagerhalle in Pratteln. 3000 m², 4 Andocktore, günstigster Flächenpreis im Vergleich. Ausserhalb Basel Stadtgebiet, jedoch gute Autobahnanbindung.', units: [ @@ -3940,8 +3794,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 2.5, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1590239926044-4131a46e3f27?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BS-EXT-088', description: 'Lagerhalle im Basler Hafen, Westquaistrasse. 2600 m², 2 Andocktore, direkte Hafenlage. Andocktoranzahl gering — Near-Miss für Anfragen mit 3+ Toren.', units: [ @@ -3985,8 +3837,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 120, ancillaryCosts: 2.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1590239926044-4131a46e3f27?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'BS-2024-089', description: 'Logistikhalle Hochbergerstrasse 60, Basel Kleinhüningen. Derzeit von Spedition AG belegt — Auszug Juli 2026 bekannt. Pre-Market-Freigabe aktiv. 2400 m², 3 Andocktore, Deckenhöhe 12 m.', units: [ @@ -4041,8 +3891,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 84, ancillaryCosts: 4.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2023-090', description: 'Grosszügige Bürofläche an der Hardstrasse, Zürich-West. 850 m² im 2. OG, Basisausbau, 20 Parkplätze. Ideales Umfeld für mittelgrosse Unternehmen. Verfügbar Oktober 2025.', units: [ @@ -4093,8 +3941,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 84, ancillaryCosts: 5.0, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1613545325278-f24b0cae1224?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2023-091', description: 'Vollausgebaute Bürofläche im 3. OG, Pfingstweidstrasse 50, Zürich-West. 920 m², FULL-Ausbau mit hohen Decken (3.2 m), 15 Parkplätze. Verfügbar Januar 2026.', units: [ @@ -4146,8 +3992,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 4.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-EXT-092', description: 'Bürofläche im 1. OG, Hohlstrasse Altstetten. 900 m², Basisausbau, 15 Parkplätze. Günstigere Lage ausserhalb Zürich-West — Kompromissoption.', units: [ @@ -4193,8 +4037,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 5.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1613545325278-f24b0cae1224?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-EXT-093', description: 'Vollausgebaute Bürofläche im Technopark, Escher-Wyss-Areal, Zürich-West. 960 m², 4. OG, FULL-Ausbau, guter Talent-Zugang.', units: [ @@ -4239,8 +4081,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 84, ancillaryCosts: 4.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2023-094', description: 'Bürofläche im 2. OG, Werkstrasse 22, Zürich Kreis 5. 880 m², Basisausbau, 18 Parkplätze. Solider Industrie-Standort mit guter ÖV-Anbindung. Verfügbar April 2026.', units: [ @@ -4293,8 +4133,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 2.8, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1572021335469-31706a17aaef?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-095', description: 'Leichtindustrie-Halle in der Industriezone Schlieren. 450 m², 1 Andockrampe, Deckenhöhe 5.5 m, ebenerdige Zufahrt. Geeignet für Leichtfertigung, Montage, Lager. Verfügbar Januar 2026.', units: [ @@ -4345,8 +4183,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 2.5, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1572021335469-31706a17aaef?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-096', description: 'Produktionshalle in der Industriezone Dietikon. 600 m², 2 Andocktore, Deckenhöhe 6 m, Dreiphasenstrom. Günstige Miete, gute Autobahnanbindung A1. Verfügbar Juli 2026.', units: [ @@ -4398,8 +4234,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 8.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZG-EXT-097', description: 'Premium-Bürofläche an der Baarerstrasse, Zug Innenstadt. 320 m², 3. OG, Klimaanlage. Steuerlich attraktiver Kanton, repräsentative Adresse.', units: [ @@ -4446,8 +4280,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 48, ancillaryCosts: 6.0, riskLevel: RiskLevel.MEDIUM, - images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'WIN-MW-098', description: 'Ladenlokal an der Marktgasse, Winterthur Innenstadt. EG, Schaufensterfront, vollständig ausgebaut. Mittlere Passantenfrequenz, gute ÖV-Anbindung.', units: [ @@ -4492,8 +4324,6 @@ export const mockProperties: Property[] = [ contractDurationMonths: 60, ancillaryCosts: 3.8, riskLevel: RiskLevel.LOW, - images: ['https://images.unsplash.com/photo-1524758631624-e2822e304c36?w=800&h=400&fit=crop'], - mapImageUrl: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=600&h=300&fit=crop', propertyNumber: 'ZH-2024-099', description: 'Bürofläche im 2. OG, Zweierstrasse 50, Zürich Kreis 3. 165 m², Basisausbau, 2 Parkplätze. Ruhige Lage mit guter ÖV-Anbindung, Tramhaltestelle 3 Min. Verfügbar Juli 2026.', units: [ @@ -4534,7 +4364,6 @@ export const mockProperties: Property[] = [ ancillaryCosts: 4.2, riskLevel: RiskLevel.LOW, description: 'Helle Bürofläche im 3. OG an der Bachmattstrasse, Zürich-Altstetten. 320 m², Basisausbau, Parkplätze im Haus. 8 Minuten zur S-Bahn Altstetten.', - images: ['https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=400&fit=crop'], organizationId: 'org-wincasa', createdAt: '2026-02-14T09:30:00Z', updatedAt: '2026-02-14T09:30:00Z', @@ -4561,7 +4390,6 @@ export const mockProperties: Property[] = [ ancillaryCosts: 2.8, riskLevel: RiskLevel.LOW, description: 'Gewerbe- und Lagerhalle im Industriegebiet Pratteln. Erdgeschoss, 580 m², 6,5 m Hallenhöhe, 2 Ladelucken, 10 Aussenparkplätze. Sofort verfügbar.', - images: ['https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=800&h=400&fit=crop'], organizationId: 'org-wincasa', createdAt: '2026-03-05T11:00:00Z', updatedAt: '2026-03-05T11:00:00Z', @@ -4588,7 +4416,6 @@ export const mockProperties: Property[] = [ ancillaryCosts: 3.5, riskLevel: RiskLevel.LOW, description: 'Repräsentative Bürofläche im 4. OG am Bollwerk, Bern. 240 m², Vollausbau, Toplage direkt beim Bahnhof Bern (3 Min.). Kein Parkplatz.', - images: ['https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=800&h=400&fit=crop'], organizationId: 'org-wincasa', createdAt: '2026-04-02T14:15:00Z', updatedAt: '2026-04-02T14:15:00Z', @@ -4615,7 +4442,6 @@ export const mockProperties: Property[] = [ ancillaryCosts: 5.0, riskLevel: RiskLevel.MEDIUM, description: 'Ladenfläche an der Stadthausstrasse Winterthur, Erdgeschoss, Schaufensterfront. 185 m², Basisausbau. Zentrale Lage, hohe Passantenfrequenz. Derzeit deaktiviert — Verhandlung sistiert.', - images: ['https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=400&fit=crop'], organizationId: 'org-wincasa', createdAt: '2026-01-20T10:00:00Z', updatedAt: '2026-04-15T08:00:00Z', @@ -4642,7 +4468,6 @@ export const mockProperties: Property[] = [ ancillaryCosts: 2.2, riskLevel: RiskLevel.LOW, description: 'Moderne Logistikhalle in Muttenz, 1200 m², 9 m Hallenhöhe, 4 Ladelucken, Stromanschluss 400 kVA. Direkter Autobahnanschluss A2/A3, 20 Aussenparkplätze.', - images: ['https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=800&h=400&fit=crop'], organizationId: 'org-wincasa', createdAt: '2026-04-28T16:00:00Z', updatedAt: '2026-04-28T16:00:00Z', @@ -4669,7 +4494,6 @@ export const mockProperties: Property[] = [ ancillaryCosts: 3.8, riskLevel: RiskLevel.MEDIUM, description: 'Bürofläche Zürichstrasse 45, Luzern-Littau. 450 m², 1. OG, Basisausbau. 8 Tiefgaragenplätze. Deaktiviert — Preisüberarbeitung ausstehend.', - images: ['https://images.unsplash.com/photo-1524758631624-e2822e304c36?w=800&h=400&fit=crop'], organizationId: 'org-wincasa', createdAt: '2025-12-10T08:00:00Z', updatedAt: '2026-03-20T12:00:00Z', @@ -4686,22 +4510,12 @@ mockProperties.forEach((p, i) => { p.leaseContractName = `Mietvertrag ${startYear}–${endYear}` }) -// Assign images sequentially per pool so no two properties ever share the same image -// (until pool wraps, which happens after ~12 properties per pool type) -const _poolCounters: Record = {} +// Bild und Lageplan kommen aus den lokalen Assets und hängen an der Objekt-ID. +// Damit trägt jede Adresse ihr eigenes Bild — die frühere Pool-Rotation liess +// verschiedene Adressen dasselbe Foto teilen. mockProperties.forEach(p => { - const district = (p.location.district ?? '').toLowerCase() - const prestige = p.softFactors?.prestige ?? 60 - const key = getPoolKey(p.assetType, district, prestige) - const poolIndex = _poolCounters[key] ?? 0 - _poolCounters[key] = poolIndex + 1 - p.images = [resolvePropertyImage({ - assetType: p.assetType, - city: p.location.city, - district: p.location.district, - prestige: p.softFactors?.prestige, - floorLevel: p.floorLevel, - propertyId: p.id, - poolIndex, - })] + const image = resolvePropertyImage({ propertyId: p.id }) + if (image) p.images = [image] + const map = resolvePropertyMapImage(p.id) + if (map) p.mapImageUrl = map }) diff --git a/src/pages/demand/AISearch.tsx b/src/pages/demand/AISearch.tsx deleted file mode 100644 index 45159bc..0000000 --- a/src/pages/demand/AISearch.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import { useEffect, useRef, useState } from 'react' -import { Box, Tab, Tabs, Typography } from '@mui/material' -import { useLocation, useNavigate } from 'react-router' -import { useQueryClient } from '@tanstack/react-query' -import { - NeedBuilderProgress, - NeedInput, - VoiceNeedInput, - WeightingEditor, - NeedBuilderErrorState, -} from '../../components/demand' -import { AISearchActionBar } from '../../components/demand/AISearchActionBar' -import { AISearchSavePreview } from '../../components/demand/AISearchSavePreview' -import { SavedProfilesTab } from '../../components/demand/SavedProfilesTab' -import { AddToPipelineDialog } from '../../components/shortlist' -import { InquiryQuickDialog } from '../../components/match-detail/InquiryQuickDialog' -import { useParseNeed } from '../../hooks/useAI' -import { useCreateNeed, useNeed, useNeedProfiles } from '../../hooks/useNeeds' -import { useDefaultWeights } from '../../hooks/useWeighting' -import { NeedBuilderStep } from '../../domain/needBuilder' -import type { ParseNeedResult, ParsedNeedCriteria, WeightingKey } from '../../domain/needBuilder' -import { generateSummary, buildNeedInput, needToParsedCriteria } from '../../services/aiSearch/needSearchMapper' - -type ActionIntent = 'search' | 'save-profile' - -export default function AISearch() { - const navigate = useNavigate() - const queryClient = useQueryClient() - - const parseNeedMutation = useParseNeed() - const createNeedMutation = useCreateNeed() - const defaultWeights = useDefaultWeights() - - 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>(defaultWeights) - const [weightingKey, setWeightingKey] = useState(0) - const [needTitle, setNeedTitle] = useState('') - const [isAnonymous, setIsAnonymous] = useState(false) - const [error, setError] = useState(null) - - const [tab, setTab] = useState(0) - const { data: savedProfiles = [] } = useNeedProfiles() - - const locationPrefillId = (useLocation().state as { prefillNeedId?: string } | null)?.prefillNeedId - const [internalPrefillId, setInternalPrefillId] = useState() - const prefillNeedId = internalPrefillId ?? locationPrefillId - const { data: prefillNeed } = useNeed(prefillNeedId ?? '') - - const isManualTextRef = useRef(false) - - useEffect(() => { - if (!prefillNeed) return - setCriteria(needToParsedCriteria(prefillNeed)) - setWeights(prefillNeed.weightingProfile as Record) - setNeedTitle(prefillNeed.companyName) - setWeightingKey(k => k + 1) - setTab(0) - }, [prefillNeed]) - - 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) - setCriteria({}) - setParseResult(null) - } - - function handleAiAutofill() { - setStep(NeedBuilderStep.PARSING) - setError(null) - parseNeedMutation.mutate(inputText, { - onSuccess: (resp) => { - 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) - }, - onError: () => { - setError('Die KI-Analyse ist fehlgeschlagen.') - setStep(NeedBuilderStep.ERROR) - }, - }) - } - - function handleAction(chosenIntent: ActionIntent) { - setIntent(chosenIntent) - setError(null) - - const resolved: ParsedNeedCriteria = criteria - const resolvedResult: ParseNeedResult | null = parseResult - - if (!hasStructuredData && inputText.trim()) { - setStep(NeedBuilderStep.PARSING) - parseNeedMutation.mutate(inputText, { - onSuccess: (resp) => { - const parsedResolved = resp.data.extractedCriteria - const parsedResult = resp.data - setCriteria(parsedResolved) - setWeights(resp.data.suggestedWeights as Record) - setWeightingKey(k => k + 1) - isManualTextRef.current = false - setInputText('') - setIsAutoGen(false) - setParseResult(resp.data) - continueAction(chosenIntent, parsedResolved, parsedResult) - }, - onError: () => { - setError('Die KI-Analyse ist fehlgeschlagen.') - setStep(NeedBuilderStep.ERROR) - }, - }) - return - } - - continueAction(chosenIntent, resolved, resolvedResult) - } - - function continueAction( - chosenIntent: ActionIntent, - resolved: ParsedNeedCriteria, - resolvedResult: ParseNeedResult | null, - ) { - if (chosenIntent === 'search') { - setStep(NeedBuilderStep.SAVING) - 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') - createNeedMutation.mutate(input, { - onSuccess: (created) => { - queryClient.invalidateQueries({ queryKey: ['matches'] }) - navigate('/demand/results', { state: { fromNeedBuilder: true, activeNeedId: created.data.id } }) - }, - onError: () => { - setError('Suche fehlgeschlagen.') - setStep(NeedBuilderStep.ERROR) - }, - }) - return - } - - 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) - } - - 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 - const input = buildNeedInput({ ...editedCriteria, isAnonymous: isAnonymous || undefined }, weights, needTitle, conf, 'ACTIVE') - createNeedMutation.mutate(input, { - onSuccess: (created) => { - queryClient.invalidateQueries({ queryKey: ['matches'] }) - navigate('/demand/results', { state: { fromNeedBuilder: true, activeNeedId: created.data.id } }) - }, - onError: () => { - 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 vals = parseResult ? Object.values(parseResult.confidenceByField) : [] - const overallConfidence = vals.length > 0 ? vals.reduce((s, v) => s + v, 0) / vals.length : 0 - - const savedCount = savedProfiles.filter(n => !n.status || n.status === 'ACTIVE' || n.status === 'DRAFT').length - - return ( - - - - - Flächensuche - - Sprechen, schreiben oder Felder ausfüllen — dann sofort suchen oder als Suchprofil speichern - - - - setTab(v)} - sx={{ borderBottom: '1px solid #e2e8f0', px: 3, bgcolor: 'white', flexShrink: 0, minHeight: 44 }} - > - - - - - {tab === 0 && ( - <> - - - - {(step === NeedBuilderStep.IDLE || step === NeedBuilderStep.PARSING) && ( - - - - - - - handleAction('search')} - onSaveProfile={() => handleAction('save-profile')} - /> - - )} - - {isSaveStep && parseResult && editedCriteria && ( - setStep(NeedBuilderStep.IDLE)} - onSave={handleSaveProfile} - /> - )} - - {step === NeedBuilderStep.ERROR && ( - - )} - - - )} - - {tab === 1 && ( - - { setInternalPrefillId(id); setTab(0) }} /> - - )} - - ) -} diff --git a/src/pages/demand/Anfragen.tsx b/src/pages/demand/Anfragen.tsx deleted file mode 100644 index c1e2306..0000000 --- a/src/pages/demand/Anfragen.tsx +++ /dev/null @@ -1,343 +0,0 @@ -import { useState, useEffect, useRef } from 'react' -import { useNavigate, useSearchParams } from 'react-router' -import { - Box, Typography, TextField, Chip, Avatar, IconButton, - InputAdornment, Alert, -} from '@mui/material' -import { Search, Send, Paperclip, ArrowLeft, Bot, Building2, Kanban } from 'lucide-react' -import { useActiveInquiries, useSendInquiryReply, useMarkThreadAsRead } from '../../hooks/useInquiries' -import { usePipelineItems, useMoveStage } from '../../hooks/usePipeline' -import { mockProperties } from '../../mock-data/properties' -import { STAGE_ORDER, STAGE_LABELS, detectKiStage } from './anfragenKiDetection' -import { AnfragenMessageBubble } from '../../components/demand/AnfragenMessageBubble' -import { AnfragenInquiryItem } from '../../components/demand/AnfragenInquiryItem' -import { INQUIRY_STATUS_META, DS_COLORS, DS_TEXT, DS_BG, DS_BORDER, DS_SURFACE } from '../../lib/ds' -import { useSessionStore } from '../../stores/sessionStore' - -// ── Config ──────────────────────────────────────────────────────────────────── - -const FILTER_TABS = [ - { key: 'all', label: 'Alle' }, - { key: 'INQUIRY', label: 'Gesendet' }, - { key: 'OFFER', label: 'Erhalten' }, -] - -// ── Anfragen page ───────────────────────────────────────────────────────────── - -export default function Anfragen() { - const navigate = useNavigate() - const [searchParams] = useSearchParams() - const { data: pipelineItems = [] } = usePipelineItems() - const { mutate: moveStage } = useMoveStage() - - - const preselectedId = searchParams.get('inquiry') - - const { currentUser } = useSessionStore() - const { data: allInquiries = [] } = useActiveInquiries({ tenantOrgId: currentUser?.organizationId }) - const sendReply = useSendInquiryReply() - const markRead = useMarkThreadAsRead() - - const [selectedId, setSelectedId] = useState(preselectedId ?? null) - const [search, setSearch] = useState('') - const [kindFilter, setKindFilter] = useState('all') - const [replyText, setReplyText] = useState('') - const [mobileShowChat, setMobileShowChat] = useState(!!preselectedId) - const [kiAlert, setKiAlert] = useState<{ title: string; stage: string } | null>(null) - const threadRef = useRef(null) - - // Default-Auswahl: erste Konversation, sobald geladen - useEffect(() => { - if (!selectedId && allInquiries.length > 0) setSelectedId(allInquiries[0].id) - }, [allInquiries, selectedId]) - - const filtered = allInquiries.filter(inq => { - const q = search.toLowerCase() - const matchesSearch = !q || - inq.tenantName.toLowerCase().includes(q) || - inq.subject.toLowerCase().includes(q) || - (inq.tenantCompany?.toLowerCase().includes(q) ?? false) || - (inq.propertyAddress?.toLowerCase().includes(q) ?? false) || - (inq.propertyManagerName?.toLowerCase().includes(q) ?? false) || - (inq.propertyManagerCompany?.toLowerCase().includes(q) ?? false) - const matchesKind = kindFilter === 'all' || (inq.kind ?? 'INQUIRY') === kindFilter - return matchesSearch && matchesKind - }) - - const selected = allInquiries.find(i => i.id === selectedId) ?? null - const totalUnread = allInquiries.reduce((sum, i) => sum + i.unreadCount, 0) - const offeredProperties = (selected?.kind === 'OFFER' ? selected.offeredPropertyIds ?? [] : []) - .map(id => mockProperties.find(p => p.id === id)).filter(Boolean) - - // Pipeline link for currently selected inquiry - const linkedPipelineItem = selected?.propertyId - ? pipelineItems.find(i => i.propertyId === selected.propertyId) - : (selected ? pipelineItems.find(i => i.inquiryId === selected.id) : undefined) - - useEffect(() => { - if (threadRef.current) { - threadRef.current.scrollTop = threadRef.current.scrollHeight - } - }, [selected?.thread.length]) - - // Auto-dismiss KI alert after 6s - useEffect(() => { - if (!kiAlert) return - const t = setTimeout(() => setKiAlert(null), 6000) - return () => clearTimeout(t) - }, [kiAlert]) - - function handleSelect(id: string) { - setSelectedId(id) - markRead.mutate(id) - setMobileShowChat(true) - setKiAlert(null) - } - - function handleSend() { - if (!replyText.trim() || !selectedId) return - const text = replyText.trim() - - sendReply.mutate({ - inquiryId: selectedId, - payload: { body: text, senderType: 'tenant', senderName: currentUser?.name ?? 'Sie' }, - }) - setReplyText('') - - // KI: detect stage transition from message content - const ki = detectKiStage(text) - if (ki && selected) { - const pipelineItem = selected.propertyId - ? pipelineItems.find(i => i.propertyId === selected.propertyId) - : pipelineItems.find(i => i.inquiryId === selectedId) - if (pipelineItem) { - const currentIdx = STAGE_ORDER.indexOf(pipelineItem.stage) - const targetIdx = STAGE_ORDER.indexOf(ki.stage) - if (targetIdx > currentIdx) { - moveStage({ id: pipelineItem.id, stage: ki.stage }) - setKiAlert({ title: pipelineItem.title, stage: STAGE_LABELS[ki.stage] }) - } - } - } - } - - return ( - - - {/* ── Left panel ── */} - - - - Anfragen - {totalUnread > 0 && ( - - )} - - setSearch(e.target.value)} - slotProps={{ input: { startAdornment: } }} - sx={{ mb: 1.25 }} - /> - - {FILTER_TABS.map(tab => ( - setKindFilter(tab.key)} - sx={{ - height: 22, fontSize: '0.7rem', cursor: 'pointer', - bgcolor: kindFilter === tab.key ? 'primary.main' : DS_BG.subtle, - color: kindFilter === tab.key ? 'white' : DS_TEXT.secondary, - fontWeight: kindFilter === tab.key ? 700 : 400, - '&:hover': { bgcolor: kindFilter === tab.key ? 'primary.dark' : DS_BG.muted }, - }} - /> - ))} - - - - - {filtered.length === 0 ? ( - - Keine Anfragen gefunden. - - ) : filtered.map(inq => ( - i.propertyId === inq.propertyId) : pipelineItems.find(i => i.inquiryId === inq.id))} - perspective="demand" - onSelect={handleSelect} - /> - ))} - - - - {/* ── Right panel: chat ── */} - - {!selected ? ( - - Anfrage auswählen - Wählen Sie links eine Anfrage aus. - - ) : ( - <> - {/* Chat header */} - - - setMobileShowChat(false)}> - - - - {(selected.propertyAddress ?? selected.subject).split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2)} - - - - {selected.propertyAddress ?? selected.subject} - {(selected.propertyManagerCompany ?? selected.propertyManagerName) && ( - - {selected.propertyManagerCompany ?? selected.propertyManagerName ?? ''} - - )} - - - {selected.subject} - - - - {selected.matchScore && ( - = 80 ? DS_SURFACE.success.bg : DS_SURFACE.warning.bg, - color: selected.matchScore >= 80 ? DS_TEXT.success : DS_TEXT.warning, - fontWeight: 700, height: 22, fontSize: '0.75rem', - border: `1px solid ${selected.matchScore >= 80 ? DS_SURFACE.success.border : DS_SURFACE.warning.border}`, - }} /> - )} - - {/* Pipeline link */} - {linkedPipelineItem && ( - } - label={STAGE_LABELS[linkedPipelineItem.stage] ?? linkedPipelineItem.stage} - size="small" - onClick={() => navigate('/demand/pipeline')} - sx={{ - height: 22, fontSize: '0.75rem', cursor: 'pointer', - bgcolor: DS_COLORS.futureCard.signal.headerBg, fontWeight: 600, - color: 'primary.main', - border: `1px solid ${DS_COLORS.futureCard.signal.border}`, - '& .MuiChip-icon': { color: 'primary.main' }, - '&:hover': { bgcolor: DS_COLORS.futureCard.signal.badgeBg }, - }} - /> - )} - - - - {/* Property reference */} - {(linkedPipelineItem?.propertyAddress ?? selected.subject) && ( - - - - {linkedPipelineItem?.propertyAddress ?? selected.subject} - - - )} - - - {/* KI stage-change alert */} - {kiAlert && ( - - } - onClose={() => setKiAlert(null)} - sx={{ py: 0.5, bgcolor: DS_COLORS.futureCard.controlled.headerBg, color: DS_COLORS.futureCard.controlled.badgeText, border: `1px solid ${DS_COLORS.futureCard.controlled.border}`, '& .MuiAlert-icon': { color: DS_COLORS.futureCard.controlled.accent } }} - > - KI erkannt: „{kiAlert.title}" wurde in der Pipeline auf {kiAlert.stage} verschoben.{' '} - navigate('/demand/pipeline')}> - Pipeline öffnen - - - - )} - - {/* Angebot: enthaltene Objekte */} - {offeredProperties.length > 0 && ( - - - Enthaltene Objekte ({offeredProperties.length}) - - - {offeredProperties.map(p => p && ( - } - label={`${p.title} · CHF ${p.rentPricePerSqm}/m²`} - size="small" - onClick={() => navigate(`/demand/property/${p.id}`)} - sx={{ cursor: 'pointer', bgcolor: DS_SURFACE.blue.bg, color: DS_TEXT.signalDark, border: `1px solid ${DS_SURFACE.blue.border}`, fontSize: '0.72rem', '& .MuiChip-icon': { color: DS_TEXT.signalDark } }} - /> - ))} - - - )} - - {/* Thread */} - - {selected.thread.map(msg => ( - - ))} - - - {/* Composer */} - - - setReplyText(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter' && e.ctrlKey) handleSend() }} - sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }} - /> - - - - - - - - - Ctrl + Enter · KI erkennt Terminvereinbarungen automatisch - - - - )} - - - ) -} diff --git a/src/pages/demand/Compare.tsx b/src/pages/demand/Compare.tsx deleted file mode 100644 index eb7de30..0000000 --- a/src/pages/demand/Compare.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { - Alert, - Box, - Button, - Card, - Chip, - Stack, - Table, - TableCell, - TableHead, - TableRow, - Typography, -} from '@mui/material' -import { useNavigate } from 'react-router' -import { useCompareStore } from '../../stores/compareStore' -import { - CompareEmptyState, - CompareColumnHeader, - AICompareSummary, - CompareCriteriaCard, - CompareTableBody, -} from '../../components/compare' -import { AddToPipelineDialog } from '../../components/shortlist' -import { - LABEL_SX, - DATA_SX, -} from '../../components/compare/compareUtils' -import { useCompareData } from '../../hooks/useCompareData' - -// ── Main component ──────────────────────────────────────────────────────────── - -export default function Compare() { - const navigate = useNavigate() - const { compareItems, removeFromCompare, clearCompare } = useCompareStore() - - const { - aiSummary, - aiLoading, - activeNeed, - relevantCriteria, - overallWinnerIdx, - bestScoreIdx, - worstConfIdx, - worstDQIdx, - missingCriticalCounts, - maxMissingCritical, - } = useCompareData(compareItems) - - if (compareItems.length === 0) { - return ( - - - Vergleich - - - - ) - } - - 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} /> - - {/* Criteria Head-to-Head */} - {activeNeed && relevantCriteria.length > 0 && ( - - )} - - - - - {/* Column headers */} - - - - Kriterium - - {compareItems.map(item => ( - - removeFromCompare(item.matchId)} /> - - ))} - - - - - -
-
- - {/* 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/src/pages/demand/MatchDetail.tsx b/src/pages/demand/MatchDetail.tsx deleted file mode 100644 index b4c2024..0000000 --- a/src/pages/demand/MatchDetail.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { InquiryQuickDialog } from '../../components/match-detail/InquiryQuickDialog' -import { Box, Button, Chip, CircularProgress, Paper, Typography } from '@mui/material' -import { AlertTriangle, ArrowLeft } from 'lucide-react' -import { useNavigate, useParams } from 'react-router' -import { useCompareStore } from '../../stores/compareStore' -import { usePipelineStore } from '../../stores/pipelineStore' -import { useInquiryStore } from '../../stores/inquiryStore' -import { AddToPipelineDialog } from '../../components/shortlist' -import { MatchReasonList } from '../../components/match-card/MatchReasonList' -import { getCityIntelligence, lookupCityCoords } from '../../lib/locationIntelligence' -import { NextActionsPanel, FitOutCostPanel, FitOutAdvicePanel, MarketPricePanel } from '../../components/match-detail' -import type { MatchCardReason } from '../../components/match-card/MatchCardViewModel' -import { useMatchDetailData } from '../../hooks/useMatchDetailData' -import { useMatchDetail } from '../../hooks/useMatches' -import { MatchDetailHero } from '../../components/match-detail/MatchDetailHero' -import { MatchDetailScoreBreakdown } from '../../components/match-detail/MatchDetailScoreBreakdown' -import { RESULT_TYPE_META, DS_TEXT, DS_SURFACE, DS_BORDER, DS_BG } from '../../lib/ds' - -// ── helpers ─────────────────────────────────────────────────────────────────── - -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, - })) -} - -// ── page ────────────────────────────────────────────────────────────────────── - -export default function MatchDetail() { - const { matchId } = useParams<{ matchId: string }>() - const navigate = useNavigate() - const { addToCompare } = useCompareStore() - const { openSavedDialog } = usePipelineStore() - const { openInquiryDialog } = useInquiryStore() - - const { match, property, need, signal, isLoading, isFuture } = useMatchDetailData(matchId ?? '') - const detailUnit = property?.units?.find(u => u.id === match?.unitId) - - if (isLoading) { - return ( - - - - ) - } - - if (!match) { - return ( - - - Match nicht gefunden - - - ) - } - - const reasons = buildReasons(match) - const rt = RESULT_TYPE_META[match.resultType ?? 'VERIFIED_PORTFOLIO'] ?? { label: '–', color: '#64748b' } - - const title = property?.title ?? signal?.companyName ?? signal?.locationHint ?? '–' - const city = property?.location?.city - const location = city - ? `${city}${property?.location?.district ? `, ${property.location.district}` : ''}` - : signal?.locationHint ?? '–' - - const allFactors = [...match.positiveFactors, ...(match.negativeFactors ?? [])] - const hasTaxFactor = allFactors.some(f => f.criterion === 'taxEnvironment') - const cityIntel = city ? getCityIntelligence(city) : null - const taxCalculatorUrl = hasTaxFactor ? (cityIntel?.taxCalculatorUrl ?? undefined) : undefined - - const searchCenter = need?.preferredLocations?.[0] ? lookupCityCoords(need.preferredLocations[0]) : null - const searchLabel = need?.preferredLocations?.[0] - - const topTradeoff = match.tradeoffs?.[0] ?? match.tradeOffs?.[0] - const summary = match.explainabilitySummary || `${match.matchStrength}-Match mit ${match.matchScore} Punkten.` - - const handleCompare = () => { - if (match && !isFuture && property) { - addToCompare({ - resultType: (property.resultType ?? 'VERIFIED_PORTFOLIO') as 'VERIFIED_PORTFOLIO' | 'MAISON_WORK', - 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 handleShortlist = () => { - openSavedDialog({ - resultId: match.id, - resultType: match.resultType ?? 'VERIFIED_PORTFOLIO', - title: property?.title ?? signal?.companyName ?? signal?.locationHint ?? match.id, - matchScore: match.matchScore, - location: property?.location?.city, - areaLabel: property?.areaSqm ? `${property.areaSqm.toLocaleString('de-CH')} m²` : undefined, - rentLabel: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²/Jahr` : undefined, - }) - } - - const keyFacts = isFuture ? [ - { label: 'Flächenschätzung', value: signal?.areaSqmEstimate ? `~${signal.areaSqmEstimate.toLocaleString('de-CH')} m²` : '–' }, - { label: 'Zeithorizont', value: signal?.timeHorizonMonths ? `~${signal.timeHorizonMonths} Monate` : '–' }, - { label: 'Wahrscheinlichkeit', value: signal?.probability ? `${Math.round(signal.probability * 100)}%` : '–' }, - ] : [ - { label: 'Nutzfläche', value: property?.areaSqm ? `${property.areaSqm.toLocaleString('de-CH')} m²` : '–' }, - { label: 'Miete/m²/Jahr', value: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}` : '–' }, - { label: 'Verfügbar ab', value: property?.availabilityDate ?? '–' }, - { label: 'Nutzungsart', value: property?.assetType ?? '–' }, - ...(property?.hardFacts?.fitOut ? [{ - label: 'Ausbaustandard', - value: (() => { - const LABELS: Record = { SHELL: 'Rohbau', BASIC: 'Basisausbau', FULL: 'Vollausbau', PREMIUM: 'Premiumausbau' } - const base = LABELS[property.hardFacts!.fitOut!] ?? property.hardFacts!.fitOut! - const mab = property.hardFacts!.mieterausbaubeitragPerSqm - return mab ? `${base} + CHF ${mab} MAB` : base - })(), - }] : []), - ] - - return ( - - - - - {/* Sticky back nav */} - - - {isFuture && ( - - )} - - - - - {/* ── Main content ── */} - - - {/* ── Section A: Warum dieser Match? ── */} - - Warum dieser Match? - 0 ? 1.5 : 0, lineHeight: 1.65 }}> - {summary} - - {reasons.length > 0 && } - {topTradeoff && ( - - - - - Hauptabwägung - - - {topTradeoff.concern} - - - - )} - - - {/* ── Section B: Nächste Aktion ── */} - openInquiryDialog({ - propertyTitle: title, - location, - areaLabel: property?.areaSqm ? `${property.areaSqm.toLocaleString('de-CH')} m²` : undefined, - rentLabel: property?.rentPricePerSqm ? `CHF ${property.rentPricePerSqm}/m²/Jahr` : undefined, - matchScore: match.matchScore, - })} - /> - - {/* ── Section C: Reale Jahresbelastung (alle fitOut-Werte) ── */} - {!isFuture && property?.hardFacts?.fitOut && ( - - )} - - {/* ── KI-Ausbauempfehlung: nur wenn Mieter ausbaut (SHELL/BASIC, kein Vermieter-Übernahme) ── */} - {!isFuture && property?.hardFacts?.fitOut - && !property.hardFacts.fitOutByLandlord - && (property.hardFacts.fitOut === 'SHELL' || property.hardFacts.fitOut === 'BASIC') && ( - - )} - - {/* ── Marktpreis-Einschätzung (Angebot vs. Markt; bei Pre-Market inkl. Zukunftspreis) ── */} - {property?.location?.city && property.rentPricePerSqm > 0 && ( - - )} - - {/* ── Full analysis toggle + expanded panels ── */} - - - - ) -} diff --git a/src/pages/demand/Pipeline.tsx b/src/pages/demand/Pipeline.tsx deleted file mode 100644 index bcee43d..0000000 --- a/src/pages/demand/Pipeline.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { useState } from 'react' -import { useNavigate } from 'react-router' -import { - Box, Chip, Typography, -} from '@mui/material' -import { - DndContext, DragOverlay, PointerSensor, useSensor, useSensors, closestCenter, -} from '@dnd-kit/core' -import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core' -import { TrendingUp } from 'lucide-react' -import { usePipelineItems, useMoveStage } from '../../hooks/usePipeline' -import { useInquiryStore } from '../../stores/inquiryStore' -import { AddToPipelineDialog } from '../../components/shortlist' -import { InquiryQuickDialog } from '../../components/match-detail/InquiryQuickDialog' -import type { PipelineItem, PipelineStage } from '../../domain/pipeline' -import { STAGES } from '../../components/pipeline/pipelineConstants' -import { DraggableCard } from '../../components/pipeline/PipelineCard' -import { DroppableColumn } from '../../components/pipeline/PipelineColumn' -import { DetailPanel } from '../../components/pipeline/PipelineDetailPanel' - -// ── Pipeline page ───────────────────────────────────────────────────────────── - -export default function Pipeline() { - const navigate = useNavigate() - const { data: items = [] } = usePipelineItems() - const { mutate: moveStage } = useMoveStage() - const openInquiryDialog = useInquiryStore(s => s.openInquiryDialog) - const [selectedItem, setSelectedItem] = useState(null) - const [activeId, setActiveId] = useState(null) - const [overId, setOverId] = useState(null) - - const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 6 } }) - ) - - const activeItem = activeId ? items.find(i => i.id === activeId) ?? null : null - const syncedSelected = selectedItem ? items.find(i => i.id === selectedItem.id) ?? null : null - - const activeCount = items.filter(i => i.stage !== 'CLOSED_WON' && i.stage !== 'CLOSED_LOST').length - const wonItems = items.filter(i => i.stage === 'CLOSED_WON') - const wonScore = wonItems.length > 0 - ? Math.round(wonItems.reduce((s, i) => s + i.matchScore, 0) / wonItems.length) - : 0 - - function handleDragStart({ active }: DragStartEvent) { - setActiveId(active.id as string) - } - - function handleDragOver({ over }: { over: { id: string | number } | null }) { - setOverId(over ? String(over.id) : null) - } - - function handleDragEnd({ active, over }: DragEndEvent) { - setActiveId(null) - setOverId(null) - if (!over) return - const targetStage = over.id as PipelineStage - const item = items.find(i => i.id === active.id) - if (item && item.stage !== targetStage) { - moveStage({ id: item.id, stage: targetStage }) - } - } - - function handleSelect(item: PipelineItem) { - if (activeId) return // ignore clicks that fire after a drag - setSelectedItem(prev => prev?.id === item.id ? null : item) - } - - return ( - - - - - {/* Header */} - - - Deal Pipeline - - Von der ersten Idee bis zum Abschluss — per Drag & Drop verschieben - - - - {wonItems.length > 0 && ( - } label={`${wonItems.length} gewonnen · ø ${wonScore}%`} size="small" - sx={{ bgcolor: '#f0fdf4', color: '#1a7a4a', fontWeight: 600, border: '1px solid #86efac' }} /> - )} - - - - - {/* Body */} - - - {/* Kanban */} - - {STAGES.map(stage => { - const columnItems = items.filter(i => i.stage === stage.key) - const isOver = overId === stage.key - - return ( - - - - {stage.label} - - - - navigate(`/demand/anfragen?inquiry=${inquiryId}`)} - onCardChatClick={(item) => openInquiryDialog({ - propertyTitle: item.title, - location: item.location ?? '', - matchScore: item.matchScore ?? 0, - propertyId: item.propertyId, - })} - isOver={isOver} - /> - - ) - })} - - - {/* Drag overlay — the card that floats under the cursor */} - - {activeItem && ( - {}} - isDragOverlay - /> - )} - - - - {/* Detail panel */} - {syncedSelected && ( - setSelectedItem(null)} /> - )} - - - ) -} diff --git a/src/pages/demand/PropertyDetail.tsx b/src/pages/demand/PropertyDetail.tsx deleted file mode 100644 index 720a67f..0000000 --- a/src/pages/demand/PropertyDetail.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useNavigate, useParams, useSearchParams } from 'react-router' -import { - Box, - Button, - CircularProgress, - Divider, - Paper, - Typography, -} from '@mui/material' -import { ArrowLeft, Building2, MapPin, ShieldCheck } from 'lucide-react' -import { usePropertyById } from '../../hooks/useProperties' -import { ASSET_LABELS } from '../../components/match-detail/MatchDetailPropertyDetails' -import { PropertyDetailPublicSections } from '../../components/match-detail/PropertyDetailPublicSections' -import { PropertyContactForm } from '../../components/demand/PropertyContactForm' - -// ── Page ────────────────────────────────────────────────────────────────────── - -export default function PropertyDetail() { - const { propertyId } = useParams<{ propertyId: string }>() - const [searchParams] = useSearchParams() - const highlightUnitId = searchParams.get('unit') - const navigate = useNavigate() - - const { data: property, isLoading } = usePropertyById(propertyId ?? '') - - if (isLoading) { - return ( - - - - ) - } - - if (!property) { - return ( - - Objekt nicht gefunden. - - ) - } - - const preMarketUnits = (property.units ?? []).filter(u => u.schattenmarktRelease?.enabled) - const monthlyPerSqm = Math.round(property.rentPricePerSqm / 12) - - return ( - - - {/* ── Back ── */} - - - {/* ── Header ── */} - - {property.images?.[0] && ( - - )} - - - - - - - {ASSET_LABELS[property.assetType] ?? property.assetType} - - - {property.title} - - - - {property.address.street} {property.address.houseNumber}, {property.address.postalCode} {property.address.city} - - - - - - CHF {monthlyPerSqm.toLocaleString('de-CH')}/m²/Mt. - - {property.areaSqm.toLocaleString('de-CH')} m² total - - - - {preMarketUnits.length > 0 && ( - - - - {preMarketUnits.length} Einheit{preMarketUnits.length !== 1 ? 'en' : ''} für Pre-Market freigegeben — noch vor offizieller Insertion - - - )} - - - - - - - - - - Diese Fläche ist noch nicht offiziell auf dem Markt. Die Verwaltung hat sie explizit für qualifizierte Suchanfragen freigegeben. Ihre Anfrage wird vertraulich behandelt. - - - ) -} diff --git a/src/pages/demand/Results.tsx b/src/pages/demand/Results.tsx deleted file mode 100644 index d1cd0c1..0000000 --- a/src/pages/demand/Results.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { useState, useMemo, useEffect } from 'react' -import { Box, Button, Typography } from '@mui/material' -import { Zap } from 'lucide-react' -import { useNavigate, useLocation } from 'react-router' -import { useQueryClient } from '@tanstack/react-query' -import { useUnifiedResults } from '../../hooks/useUnifiedResults' -import { useNeeds } from '../../hooks/useNeeds' -import { - FeedEmptyState, - FeedSkeleton, - ResultFeedHeader, - ResultFilterBar, - UnifiedResultFeed, -} from '../../components/results' -import { AddToPipelineDialog } from '../../components/shortlist' -import { InquiryQuickDialog } from '../../components/match-detail/InquiryQuickDialog' -import { ErrorState } from '../../components/ui' -import { useSessionStore } from '../../stores/sessionStore' -import type { UnifiedMatchResult } from '../../domain/unifiedResult' - -type FilterSource = 'ALL' | 'PLATFORM' | 'MAISON_WORK' -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 { currentUser } = useSessionStore() - const [filterSource, setFilterSource] = useState('ALL') - const [sortBy, setSortBy] = useState('score') - const [showFutureAvailability, setShowFutureAvailability] = useState(true) - const [showOwnProperties, setShowOwnProperties] = useState(true) - const [view, setView] = useState<'list' | 'grid'>(() => - (localStorage.getItem('view-results') as 'list' | 'grid') ?? 'list' - ) - const [columns, setColumns] = useState<3 | 5 | 10>(() => - (Number(localStorage.getItem('view-results-cols')) as 3 | 5 | 10) || 3 - ) - - // When coming from NeedBuilder, invalidate so the freshly created need is included - const activeNeedIdFromNav = (location.state as { activeNeedId?: string } | null)?.activeNeedId - - const { data: allNeeds = [] } = useNeeds({ - refetchOnMount: activeNeedIdFromNav ? 'always' : true, - gcTime: 0, - }) - - // Nav ID (from NeedBuilder) takes priority; otherwise first named need - const effectiveNeedId = activeNeedIdFromNav ?? allNeeds.find(n => n.companyName !== 'Neue Suche')?.id - - const activeNeed = allNeeds.find(n => n.id === effectiveNeedId) ?? allNeeds[0] - - // Invalidate when navigating from NeedBuilder — must be in an effect, not render body - useEffect(() => { - if (activeNeedIdFromNav) { - queryClient.invalidateQueries({ queryKey: ['needs'] }) - } - }, [activeNeedIdFromNav, queryClient]) - - const needsLoading = allNeeds.length === 0 && !activeNeedIdFromNav - const { data: results = [], isLoading, error } = useUnifiedResults(needsLoading ? undefined : effectiveNeedId) - - const isStaff = currentUser?.role === 'PROPERTY_MANAGER' || currentUser?.role === 'ORGANIZATION_ADMIN' - - // Counts over the full result set — single pass, stable across filter changes - const { platformCount, maisonWorkCount, futureCount } = useMemo(() => { - let platform = 0, maison = 0, future = 0 - for (const r of results) { - if (r.resultType === 'VERIFIED_PORTFOLIO') platform++ - else if (r.resultType === 'MAISON_WORK') maison++ - else if (r.resultType === 'FUTURE_AVAILABILITY') future++ - } - return { platformCount: platform, maisonWorkCount: maison, futureCount: future } - }, [results]) - - // Filter + sort in one memo — only reruns when inputs change - const sorted = useMemo(() => { - const filtered = results.filter(r => { - if (r.resultType === 'FUTURE_AVAILABILITY') return showFutureAvailability - if (r.resultType === 'VERIFIED_PORTFOLIO') { - if (!showOwnProperties) return false - return filterSource === 'ALL' || filterSource === 'PLATFORM' - } - if (filterSource === 'ALL') return true - return r.resultType === filterSource - }) - return sortResults(filtered, sortBy) - }, [results, filterSource, sortBy, showFutureAvailability, showOwnProperties]) - - - return ( - - - - { setView(v); localStorage.setItem('view-results', v) }} - columns={columns} - onColumnsChange={n => { setColumns(n); localStorage.setItem('view-results-cols', String(n)) }} - /> - - - - {/* Aktive Suche */} - {activeNeed && ( - - - - {activeNeed.companyName} - - {activeNeed.assetType} - {activeNeed.requiredArea.min}–{activeNeed.requiredArea.max} m² - {activeNeed.preferredLocations.join(', ')} - {activeNeed.budgetRange?.maxPerSqm > 0 && ( - max. CHF {activeNeed.budgetRange.maxPerSqm}/m²/J - )} - - - - )} - - - - {isLoading ? ( - - ) : error ? ( - - ) : sorted.length === 0 ? ( - - ) : ( - <> - {futureCount > 0 && showFutureAvailability && ( - - - - - {futureCount} Zukunftssignal{futureCount > 1 ? 'e' : ''} in diesen Ergebnissen - - - Noch nicht öffentlich verfügbare Flächen — gewonnen aus Mietvertrags-Auslaufzeiten, Bauprojekten und Marktbewegungen. Frühere Markttransparenz für Ihre Suche. - - - - )} - - - )} - - - ) -} diff --git a/src/pages/demand/Shortlists.tsx b/src/pages/demand/Shortlists.tsx deleted file mode 100644 index 2690a3a..0000000 --- a/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/src/pages/demand/anfragenKiDetection.ts b/src/pages/demand/anfragenKiDetection.ts deleted file mode 100644 index 06a3190..0000000 --- a/src/pages/demand/anfragenKiDetection.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { PipelineStage } from '../../domain/pipeline' - -export const STAGE_ORDER: PipelineStage[] = [ - 'SAVED', 'CONTACTED', 'VISITED', 'NEGOTIATION', 'CLOSED_WON', 'CLOSED_LOST', -] - -export const STAGE_LABELS: Record = { - SAVED: 'Interessiert', CONTACTED: 'Angefragt', - VISITED: 'Besichtigt', NEGOTIATION: 'Verhandlung', CLOSED_WON: 'Gewonnen', CLOSED_LOST: 'Abgelehnt', -} - -export const KI_RULES: { keywords: string[]; stage: PipelineStage; label: string }[] = [ - { - keywords: ['vertrag unterschrieben', 'unterschrieben', 'deal abgeschlossen', 'abgeschlossen und fix', 'mietbeginn bestätigt'], - stage: 'CLOSED_WON', - label: 'Abschluss erkannt', - }, - { - keywords: ['mietvertrag', 'vertragsvorlage', 'anbiet', 'konditionen verhandl', 'preisvorstellung'], - stage: 'NEGOTIATION', - label: 'Verhandlung erkannt', - }, - { - keywords: ['besichtigungstermin', 'besichtigung', 'besichtigen', 'vorort termin', 'vor ort', 'terminvorschlag', 'termin bestätigt', 'termin vereinbart'], - stage: 'VISITED', - label: 'Besichtigungstermin erkannt', - }, -] - -export function detectKiStage(text: string): { stage: PipelineStage; label: string } | null { - const lower = text.toLowerCase() - for (const rule of KI_RULES) { - if (rule.keywords.some(kw => lower.includes(kw))) { - return { stage: rule.stage, label: rule.label } - } - } - return null -} diff --git a/src/pages/supply/Anfragencenter.tsx b/src/pages/supply/Anfragencenter.tsx deleted file mode 100644 index 17f8aea..0000000 --- a/src/pages/supply/Anfragencenter.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useState } from 'react' -import { useLocation } from 'react-router' -import { Box, Tab, Tabs, Typography } from '@mui/material' -import { ActiveInquiriesTab, LatentInquiriesTab, OfferWizard } from '../../components/anfragencenter' -import { useSessionStore } from '../../stores/sessionStore' - -export default function Anfragencenter() { - const location = useLocation() - const initialTab = (location.state as { tab?: number } | null)?.tab ?? 0 - const [tab, setTab] = useState(initialTab) - const orgId = useSessionStore(s => s.currentUser?.organizationId) - return ( - - - - Anfragencenter - - setTab(v)} - sx={{ - '& .MuiTab-root': { textTransform: 'none', fontWeight: 600, fontSize: '0.95rem' }, - }} - > - - - - - - - {tab === 0 && } - {tab === 1 && ( - - )} - {tab === 2 && } - - - - ) -} diff --git a/src/pages/supply/Personalverwaltung.tsx b/src/pages/supply/Personalverwaltung.tsx index 5673eb4..907f0ab 100644 --- a/src/pages/supply/Personalverwaltung.tsx +++ b/src/pages/supply/Personalverwaltung.tsx @@ -1,44 +1,34 @@ import { useCallback, useEffect, useMemo } from 'react' -import { useNavigate, useParams, useSearchParams } from 'react-router' -import { Box, ToggleButton, ToggleButtonGroup, useMediaQuery, useTheme } from '@mui/material' -import { CircleDot, Rows3 } from 'lucide-react' +import { useNavigate, useParams } from 'react-router' +import { Box, useMediaQuery, useTheme } from '@mui/material' import { TeamPageHeader, AgentListPanel, AgentDossier, - RotatableAgentWheel, resolveDossierSegment, } from '../../components/team' import type { DossierSegment } from '../../components/team' import { EmptyState, ErrorState, PanelLoadingState } from '../../components/ui' -import { agentDirectory } from '../../mock-data/agentDirectory' import { useTeamAgents } from '../../hooks/useTeamAgents' import { ROUTES } from '../../lib/constants' import { DS_BG } from '../../lib/ds' -const VIEW_PARAM = 'view' -const VIEW = { DOSSIER: 'dossier', ORG: 'organigramm' } as const -type PersonnelView = typeof VIEW[keyof typeof VIEW] - -function isView(value: string | null): value is PersonnelView { - return value === VIEW.DOSSIER || value === VIEW.ORG -} - -/** Nur die sieben Kernteammitglieder — sie besitzen ein Personaldossier. */ -const coreEntries = agentDirectory.filter(a => a.isCore) - +/** + * Personalverwaltung — Liste links, Personaldossier rechts. + * + * Die frühere Organigramm-Ansicht mit drehbarem Halbkreis ist entfallen, mit + * ihr der Umschalter im Kopfbereich. Es gibt nur noch eine Ansicht, also braucht + * es auch keine Wahl mehr; ein Umschalter mit einem einzigen Ziel wäre reine + * Zierde. + */ export default function Personalverwaltung() { const { agentId, tab } = useParams<{ agentId?: string; tab?: string }>() - const [searchParams, setSearchParams] = useSearchParams() const navigate = useNavigate() const theme = useTheme() const isCompact = useMediaQuery(theme.breakpoints.down('lg')) const { data: agents = [], isLoading, isError, refetch } = useTeamAgents() - const view: PersonnelView = isView(searchParams.get(VIEW_PARAM)) - ? (searchParams.get(VIEW_PARAM) as PersonnelView) - : VIEW.DOSSIER const activeSegment: DossierSegment = resolveDossierSegment(tab) const selectedAgent = useMemo(() => agents.find(a => a.id === agentId) ?? null, [agents, agentId]) @@ -46,15 +36,15 @@ export default function Personalverwaltung() { // Dossier wäre für den Nutzer eine Sackgasse. useEffect(() => { if (!agentId && agents.length > 0) { - navigate(`${ROUTES.SUPPLY.TEAM_PERSONNEL}/${agents[0].id}?${VIEW_PARAM}=${view}`, { replace: true }) + navigate(`${ROUTES.SUPPLY.TEAM_PERSONNEL}/${agents[0].id}`, { replace: true }) } - }, [agentId, agents, navigate, view]) + }, [agentId, agents, navigate]) const goTo = useCallback( (nextAgentId: string, segment: DossierSegment) => { - navigate(`${ROUTES.SUPPLY.TEAM_PERSONNEL}/${nextAgentId}/${segment}?${VIEW_PARAM}=${view}`) + navigate(`${ROUTES.SUPPLY.TEAM_PERSONNEL}/${nextAgentId}/${segment}`) }, - [navigate, view], + [navigate], ) const selectAgent = useCallback( @@ -69,47 +59,9 @@ export default function Personalverwaltung() { [goTo, agentId], ) - const changeView = useCallback( - (next: PersonnelView | null) => { - if (!next) return - const params = new URLSearchParams(searchParams) - params.set(VIEW_PARAM, next) - setSearchParams(params, { replace: false }) - }, - [searchParams, setSearchParams], - ) - - const dossier = selectedAgent && ( - - ) - return ( - changeView(next)} - aria-label="Ansicht" - > - - - Dossieransicht - - - - Organigramm - - - } - /> + {isError ? ( refetch()} /> @@ -120,18 +72,7 @@ export default function Personalverwaltung() { title="Kein Mitarbeitender gewählt" description="Wählen Sie einen digitalen Mitarbeitenden, um sein Personaldossier zu öffnen." /> - ) : view === VIEW.ORG ? ( - /* Organigramm: Halbkreis oben, Dossier des aktiven Mitarbeitenden darunter */ - - - {dossier} - ) : ( - /* Dossieransicht: Agentenliste links, Dossier rechts */ - {dossier} + )} diff --git a/src/pages/supply/Properties.tsx b/src/pages/supply/Properties.tsx index 1c29ebb..65276f7 100644 --- a/src/pages/supply/Properties.tsx +++ b/src/pages/supply/Properties.tsx @@ -1,6 +1,5 @@ import { useState, useMemo } from 'react' -import { Box, Button, Chip, Collapse, Drawer, Typography, useMediaQuery, useTheme } from '@mui/material' -import { useNavigate } from 'react-router' +import { Box, Collapse, Drawer, Typography, useMediaQuery, useTheme } from '@mui/material' import { ChevronDown, ChevronUp } from 'lucide-react' import { PageHeader } from '../../components/layout' import { ViewToggle } from '../../components/shared' @@ -48,8 +47,17 @@ function applyFilters(properties: Property[], filters: PropertyTableFilters): Pr return result } +/** + * Kartenraster: Desktop vier, Tablet zwei, Mobil eine Karte je Zeile. + * Bewusst fest verdrahtet — die frühere Dichteauswahl (3/5/10) ist entfallen. + */ +const CARD_GRID_COLUMNS = { + xs: '1fr', + sm: 'repeat(2, minmax(0, 1fr))', + lg: 'repeat(4, minmax(0, 1fr))', +} as const + export default function Properties() { - const navigate = useNavigate() const theme = useTheme() const isMobile = useMediaQuery(theme.breakpoints.down('md')) const [selectedId, setSelectedId] = useState(null) @@ -57,9 +65,6 @@ export default function Properties() { const [view, setView] = useState<'list' | 'grid'>(() => (localStorage.getItem('view-properties') as 'list' | 'grid') ?? 'list' ) - const [columns, setColumns] = useState<3 | 5 | 10>(() => - (Number(localStorage.getItem('view-properties-cols')) as 3 | 5 | 10) || 3 - ) const [headerOpen, setHeaderOpen] = useState(() => localStorage.getItem('props-header-open') !== 'false' ) @@ -77,66 +82,16 @@ export default function Properties() { const filtered = useMemo(() => applyFilters(properties, filters), [properties, filters]) - // Decision-relevant aggregates — single pass over the full list - const { matchReady, criticalGaps, staleOrOutdated } = useMemo(() => { - const matchReady: typeof properties = [] - const criticalGaps: typeof properties = [] - const lowConfidence: typeof properties = [] - const staleOrOutdated: typeof properties = [] - const missingSet = new Set() - - for (const p of properties) { - if ((p.availabilityStatus === 'AVAILABLE_NOW' || p.availabilityStatus === 'AVAILABLE_SOON') && - p.confidenceScore >= 0.7 && p.dataQuality.missingCriticalFields.length === 0) - matchReady.push(p) - if (p.dataQuality.missingCriticalFields.length > 0) { - criticalGaps.push(p) - p.dataQuality.missingCriticalFields.forEach(f => missingSet.add(f)) - } - if (p.confidenceScore < 0.55) lowConfidence.push(p) - if (p.dataQuality.freshness === 'STALE' || p.dataQuality.freshness === 'OUTDATED') staleOrOutdated.push(p) - } - - return { - matchReady, - criticalGaps, - lowConfidence, - staleOrOutdated, - allMissingFields: [...missingSet].slice(0, 4), - } - }, [properties]) - return ( - {view === 'grid' && ( - - {([3, 5, 10] as const).map(n => ( - { setColumns(n); localStorage.setItem('view-properties-cols', String(n)) }} - sx={{ - width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', - cursor: 'pointer', fontSize: '0.72rem', fontWeight: 600, - bgcolor: columns === n ? '#152642' : 'transparent', - color: columns === n ? 'white' : '#64748b', - '&:hover': { bgcolor: columns === n ? '#162d4a' : '#f1f5f9' }, - }} - > - {n} - - ))} - - )} - { setView(v); localStorage.setItem('view-properties', v) }} - /> - + { setView(v); localStorage.setItem('view-properties', v) }} + /> } /> @@ -167,65 +122,15 @@ export default function Properties() { {headerOpen ? : } + {/* Der Datenpflege-Hinweis ist entfallen — die Datenpflege bleibt als + eigener Hauptreiter erreichbar und braucht hier keinen zweiten Einstieg. */} - {!isLoading && properties.length > 0 && ( - - - 0 ? '#f0fdf4' : '#fef9c3', - color: matchReady.length > 0 ? '#1a7a4a' : '#92400e' }} - /> - {criticalGaps.length > 0 && ( - - )} - {staleOrOutdated.length > 0 && ( - - )} - - - - )} - {view === 'grid' ? ( - + {filtered.map(p => ( ))} diff --git a/src/pages/supply/SupplyDashboard.tsx b/src/pages/supply/SupplyDashboard.tsx index bfb81f4..e331e28 100644 --- a/src/pages/supply/SupplyDashboard.tsx +++ b/src/pages/supply/SupplyDashboard.tsx @@ -125,7 +125,7 @@ export default function SupplyDashboard() { cursor: 'pointer', '&:hover': { bgcolor: DS_BG.subtle }, }} - onClick={() => navigate(`/demand/match/${m.matchId}`)} + onClick={() => navigate('/supply/match-center')} > navigate('/demand/results')} + onClick={() => navigate('/supply/match-center')} sx={{ textTransform: 'none', color: DS_TEXT.secondary, fontWeight: 500 }} > Alle Matches anzeigen → diff --git a/src/pages/supply/Teamuebersicht.tsx b/src/pages/supply/Teamuebersicht.tsx index af26abe..c410fe8 100644 --- a/src/pages/supply/Teamuebersicht.tsx +++ b/src/pages/supply/Teamuebersicht.tsx @@ -1,15 +1,12 @@ -import { useState } from 'react' import { Box } from '@mui/material' import { TeamPageHeader, TeamSectionHeader, TeamKpiSection, - TeamLevelTabs, TeamOverviewRing, ConnectionSummaryList, } from '../../components/team' import { ErrorState } from '../../components/ui' -import type { AgentLevel } from '../../domain/agentDirectory' import { AgentLevel as Level } from '../../domain/agentDirectory' import { useTeamAgents } from '../../hooks/useTeamAgents' import { useAgentConnections } from '../../hooks/useAgentConnections' @@ -25,9 +22,12 @@ import { ROUTES } from '../../lib/constants' * * Kopfbereich bewusst ohne Beschreibungstext und ohne «Demo zurücksetzen»: der * Platz gehört der Visualisierung. + * + * Gezeigt wird ausschliesslich das Kernteam. Die Stufenfilter über der Grafik + * sind entfallen — die übrigen Stufen sind in diesem Branch nicht mehr + * einsehbar. */ export default function Teamuebersicht() { - const [level, setLevel] = useState(Level.CORE) const { data: agents = [], isError, refetch } = useTeamAgents() const { data: connections = [] } = useAgentConnections() @@ -42,10 +42,7 @@ export default function Teamuebersicht() { ) : ( <> - - - - + diff --git a/src/services/authService.ts b/src/services/authService.ts index 07238d7..464937f 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -8,7 +8,7 @@ import { queryClient } from '../lib/queryClient' // Mock organizations for org switching const MOCK_ORGANIZATIONS: { id: string; name: string }[] = [ - { id: 'org-wincasa', name: 'Wincasa AG' }, + { id: 'org-wincasa', name: 'Beispiel Immobilien AG' }, { id: 'org-mobimo', name: 'Mobimo Management AG' }, { id: 'org-ubs', name: 'UBS Asset Management RE' }, ] @@ -21,7 +21,7 @@ const DEMO_USERS: Record = { name: 'Super Admin', role: UserRole.SUPER_ADMIN, organizationId: 'org-wincasa', - organizationName: 'Wincasa AG', + organizationName: 'Beispiel Immobilien AG', allowedWorkspaces: [WorkspaceType.SUPPLY, WorkspaceType.DEMAND], }, [UserRole.ORGANIZATION_ADMIN]: { @@ -30,7 +30,7 @@ const DEMO_USERS: Record = { name: 'Admin User', role: UserRole.ORGANIZATION_ADMIN, organizationId: 'org-wincasa', - organizationName: 'Wincasa AG', + organizationName: 'Beispiel Immobilien AG', allowedWorkspaces: [WorkspaceType.SUPPLY, WorkspaceType.DEMAND], }, [UserRole.PROPERTY_MANAGER]: { @@ -39,7 +39,7 @@ const DEMO_USERS: Record = { name: 'Property Manager', role: UserRole.PROPERTY_MANAGER, organizationId: 'org-wincasa', - organizationName: 'Wincasa AG', + organizationName: 'Beispiel Immobilien AG', allowedWorkspaces: getAccessibleWorkspaces(UserRole.PROPERTY_MANAGER), }, [UserRole.REVIEWER]: { @@ -48,7 +48,7 @@ const DEMO_USERS: Record = { name: 'Reviewer', role: UserRole.REVIEWER, organizationId: 'org-wincasa', - organizationName: 'Wincasa AG', + organizationName: 'Beispiel Immobilien AG', allowedWorkspaces: getAccessibleWorkspaces(UserRole.REVIEWER), }, [UserRole.OWNER_VIEWER]: { @@ -57,7 +57,7 @@ const DEMO_USERS: Record = { name: 'Owner Viewer', role: UserRole.OWNER_VIEWER, organizationId: 'org-wincasa', - organizationName: 'Wincasa AG', + organizationName: 'Beispiel Immobilien AG', allowedWorkspaces: getAccessibleWorkspaces(UserRole.OWNER_VIEWER), }, [UserRole.DEMAND_USER]: { @@ -91,7 +91,7 @@ export const authService = { name: 'Admin User', role: UserRole.ORGANIZATION_ADMIN, organizationId: 'org-wincasa', - organizationName: 'Wincasa AG', + organizationName: 'Beispiel Immobilien AG', allowedWorkspaces: [WorkspaceType.SUPPLY, WorkspaceType.DEMAND], } useSessionStore.getState().login(user) diff --git a/src/stores/sessionStore.ts b/src/stores/sessionStore.ts index 4466cdb..b8ea72e 100644 --- a/src/stores/sessionStore.ts +++ b/src/stores/sessionStore.ts @@ -37,7 +37,7 @@ const mockUser: MockUser = { name: 'Admin User', role: UserRole.ORGANIZATION_ADMIN, organizationId: 'org-wincasa', - organizationName: 'Wincasa AG', + organizationName: 'Beispiel Immobilien AG', allowedWorkspaces: [WorkspaceType.SUPPLY, WorkspaceType.DEMAND], } diff --git a/vite.config.ts b/vite.config.ts index 13165d6..c7d55d1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,6 +7,18 @@ export default defineConfig({ react(), tailwindcss(), ], + build: { + /** + * Objektbilder bleiben immer eigene Dateien. + * + * Vite bettet Assets unter 4 KB als Base64 ein. Zwei der Objektbilder liegen + * knapp darunter und landeten dadurch im JavaScript statt im Dateisystem — + * werden sie später durch echte Fotos ersetzt, sollen alle Objekte gleich + * behandelt sein. Für alle übrigen Assets bleibt die Voreinstellung. + */ + assetsInlineLimit: (filePath: string) => + filePath.includes('/assets/properties/') ? false : undefined, + }, server: { proxy: { // In dev: forward /api/* to the local PowerOn backend.