feat: role-aware UX, market intelligence, score fix & layout scroll
- Role-based workspace access: Property Manager gets Supply+Demand, Operations restricted to Super Admin + Reviewer only - Root redirect routes each persona to their first workspace - Match Center: replaced 3-panel with auto-sorted flat list + drawer - AI Search: unified form with dual-action (Jetzt suchen / Als Suchprofil speichern) - CompareTray hidden on non-Demand routes; Vergleichen removed from Supply - LocationIntelligencePanel: city KPIs, rent trends, soft factors, comparables - NegotiationInsightsPanel: price positioning, active demand, selling arguments - scoreCalculator: cap hardMatchScore and softFactorScore to max 100 - Layout: add display:flex to overflow:hidden wrappers so inner scroll works (MatchCenter drawer, MarketIntelligence, SourceMonitoring, SignalPipeline) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// Static location intelligence data per city — mock values based on Swiss market context
|
||||
|
||||
export interface CityIntelligence {
|
||||
vacancyRatePct: number // Leerstandsquote %
|
||||
rentTrend12m: number // Mietpreisveränderung % (letztes Jahr)
|
||||
purchasingPowerIndex: number // Kaufkraft-Index (CH = 100)
|
||||
dominantIndustryClusters: string[]
|
||||
plannedInfrastructure: { project: string; timeline: string; impact: string }[]
|
||||
medianRentOffice: number // CHF/m² für Bürofläche
|
||||
medianRentLogistics: number
|
||||
medianRentRetail: number
|
||||
avgDaysOnMarket: number // Durchschnittliche Tage bis Vermietung
|
||||
demandStrength: 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'
|
||||
taxIndexCanton: number // Steuerindex 100 = CH-Mittel
|
||||
}
|
||||
|
||||
export const CITY_INTELLIGENCE: Record<string, CityIntelligence> = {
|
||||
'Zürich': {
|
||||
vacancyRatePct: 2.8,
|
||||
rentTrend12m: +4.2,
|
||||
purchasingPowerIndex: 128,
|
||||
dominantIndustryClusters: ['Finanz & Banking', 'Tech & Startups', 'Medien & Kreativ', 'Pharma & Life Science'],
|
||||
plannedInfrastructure: [
|
||||
{ project: 'Tram Hardbrücke-Verlängerung', timeline: '2027', impact: 'Bessere ÖV-Anbindung Industriequartier' },
|
||||
{ project: 'Rosengarten-Tunnel', timeline: '2030', impact: 'Entlastung Kreis 5/6, weniger Durchgangsverkehr' },
|
||||
],
|
||||
medianRentOffice: 42,
|
||||
medianRentLogistics: 14,
|
||||
medianRentRetail: 180,
|
||||
avgDaysOnMarket: 38,
|
||||
demandStrength: 'VERY_HIGH',
|
||||
taxIndexCanton: 100,
|
||||
},
|
||||
'Basel': {
|
||||
vacancyRatePct: 4.1,
|
||||
rentTrend12m: +1.8,
|
||||
purchasingPowerIndex: 112,
|
||||
dominantIndustryClusters: ['Pharma & Chemie', 'Logistik & Handel', 'Medizintechnik', 'Finanzdienstleistungen'],
|
||||
plannedInfrastructure: [
|
||||
{ project: 'Basel SBB Südeingang Neubau', timeline: '2026', impact: 'Aufwertung Bahnhofumgebung' },
|
||||
{ project: 'Regio-S-Bahn Ausbau', timeline: '2028', impact: 'Bessere Grenzpendler-Anbindung' },
|
||||
],
|
||||
medianRentOffice: 32,
|
||||
medianRentLogistics: 11,
|
||||
medianRentRetail: 120,
|
||||
avgDaysOnMarket: 52,
|
||||
demandStrength: 'HIGH',
|
||||
taxIndexCanton: 98,
|
||||
},
|
||||
'Bern': {
|
||||
vacancyRatePct: 3.5,
|
||||
rentTrend12m: +2.1,
|
||||
purchasingPowerIndex: 108,
|
||||
dominantIndustryClusters: ['Bundesverwaltung & NPO', 'Gesundheit', 'Bildung & Forschung', 'Versicherungen'],
|
||||
plannedInfrastructure: [
|
||||
{ project: 'Bernmobil Netzausbau West', timeline: '2026', impact: 'Erschliessung Entwicklungsgebiet Ausserholligen' },
|
||||
],
|
||||
medianRentOffice: 28,
|
||||
medianRentLogistics: 10,
|
||||
medianRentRetail: 95,
|
||||
avgDaysOnMarket: 61,
|
||||
demandStrength: 'MEDIUM',
|
||||
taxIndexCanton: 112,
|
||||
},
|
||||
'Zug': {
|
||||
vacancyRatePct: 1.9,
|
||||
rentTrend12m: +5.1,
|
||||
purchasingPowerIndex: 148,
|
||||
dominantIndustryClusters: ['Rohstoffhandel', 'Crypto & Blockchain', 'Holding & Finanzen', 'Tech-Unternehmen'],
|
||||
plannedInfrastructure: [
|
||||
{ project: 'Metrobahn Zug-Luzern', timeline: '2029', impact: 'Direktverbindung Luzern in 18 Min.' },
|
||||
],
|
||||
medianRentOffice: 38,
|
||||
medianRentLogistics: 13,
|
||||
medianRentRetail: 140,
|
||||
avgDaysOnMarket: 24,
|
||||
demandStrength: 'VERY_HIGH',
|
||||
taxIndexCanton: 60,
|
||||
},
|
||||
'Winterthur': {
|
||||
vacancyRatePct: 5.8,
|
||||
rentTrend12m: +0.9,
|
||||
purchasingPowerIndex: 98,
|
||||
dominantIndustryClusters: ['Industrie & Maschinenbau', 'Logistik', 'Gesundheit & Soziales'],
|
||||
plannedInfrastructure: [
|
||||
{ project: 'Stadtraum HB Winterthur', timeline: '2027', impact: 'Aufwertung Bahnhofsumgebung, mehr Frequenz' },
|
||||
],
|
||||
medianRentOffice: 22,
|
||||
medianRentLogistics: 9,
|
||||
medianRentRetail: 75,
|
||||
avgDaysOnMarket: 74,
|
||||
demandStrength: 'MEDIUM',
|
||||
taxIndexCanton: 119,
|
||||
},
|
||||
'Geneva': {
|
||||
vacancyRatePct: 2.2,
|
||||
rentTrend12m: +3.6,
|
||||
purchasingPowerIndex: 135,
|
||||
dominantIndustryClusters: ['Internationale Organisationen', 'Luxusgüter', 'Banking & Private Equity', 'Uhrenindustrie'],
|
||||
plannedInfrastructure: [
|
||||
{ project: 'CEVA Linie Verlängerung', timeline: '2026', impact: 'Bessere Verbindung Lancy-Pont-Rouge' },
|
||||
],
|
||||
medianRentOffice: 55,
|
||||
medianRentLogistics: 18,
|
||||
medianRentRetail: 220,
|
||||
avgDaysOnMarket: 31,
|
||||
demandStrength: 'HIGH',
|
||||
taxIndexCanton: 125,
|
||||
},
|
||||
'St.Gallen': {
|
||||
vacancyRatePct: 6.2,
|
||||
rentTrend12m: -0.5,
|
||||
purchasingPowerIndex: 95,
|
||||
dominantIndustryClusters: ['Textil & Mode', 'KMU', 'Logistik', 'Gesundheit'],
|
||||
plannedInfrastructure: [],
|
||||
medianRentOffice: 19,
|
||||
medianRentLogistics: 8,
|
||||
medianRentRetail: 65,
|
||||
avgDaysOnMarket: 88,
|
||||
demandStrength: 'LOW',
|
||||
taxIndexCanton: 107,
|
||||
},
|
||||
}
|
||||
|
||||
export function getCityIntelligence(city: string): CityIntelligence | null {
|
||||
// Try exact match first, then partial
|
||||
if (CITY_INTELLIGENCE[city]) return CITY_INTELLIGENCE[city]
|
||||
const key = Object.keys(CITY_INTELLIGENCE).find(k => city.toLowerCase().includes(k.toLowerCase()))
|
||||
return key ? CITY_INTELLIGENCE[key] : null
|
||||
}
|
||||
|
||||
export function getMarketRent(city: string, assetType: string): number | null {
|
||||
const intel = getCityIntelligence(city)
|
||||
if (!intel) return null
|
||||
if (assetType === 'OFFICE') return intel.medianRentOffice
|
||||
if (assetType === 'LOGISTICS' || assetType === 'LIGHT_INDUSTRIAL') return intel.medianRentLogistics
|
||||
if (assetType === 'RETAIL') return intel.medianRentRetail
|
||||
return intel.medianRentOffice
|
||||
}
|
||||
@@ -29,6 +29,7 @@ const ROLE_PERMISSIONS: Record<UserRole, Permission[]> = {
|
||||
[UserRole.PROPERTY_MANAGER]: [
|
||||
Permission.SUPPLY_VIEW,
|
||||
Permission.SUPPLY_EDIT,
|
||||
Permission.DEMAND_VIEW,
|
||||
Permission.FUTURE_SIGNAL_VIEW,
|
||||
Permission.FUTURE_SIGNAL_REVIEW,
|
||||
Permission.CONTACT_RELEASE_APPROVE,
|
||||
@@ -63,11 +64,11 @@ const WORKSPACE_ROLES: Record<WorkspaceType, UserRole[]> = {
|
||||
[WorkspaceType.DEMAND]: [
|
||||
UserRole.SUPER_ADMIN,
|
||||
UserRole.ORGANIZATION_ADMIN,
|
||||
UserRole.PROPERTY_MANAGER,
|
||||
UserRole.DEMAND_USER,
|
||||
],
|
||||
[WorkspaceType.OPERATIONS]: [
|
||||
UserRole.SUPER_ADMIN,
|
||||
UserRole.ORGANIZATION_ADMIN,
|
||||
UserRole.REVIEWER,
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user