diff --git a/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.module.css b/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.module.css index 3e7797d..ba0c204 100644 --- a/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.module.css +++ b/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.module.css @@ -125,6 +125,24 @@ color: var(--color-text-secondary, #6b7280); } +.documentsSection { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 2px solid var(--color-primary, #3b82f6); + background-color: var(--color-bg-secondary, #f9fafb); + padding: 1.5rem; + border-radius: 8px; + margin-left: -1.5rem; + margin-right: -1.5rem; +} + +.documentsSectionTitle { + margin: 0 0 1rem 0; + font-size: 1.1rem; + font-weight: 600; + color: var(--color-text, #111827); +} + .infoGrid { display: flex; flex-direction: column; @@ -241,6 +259,670 @@ overflow-y: auto; } +.documentsList { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.documentLink { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 1rem; + background-color: var(--color-bg, #ffffff); + border: 2px solid var(--color-primary, #3b82f6); + border-radius: 8px; + cursor: pointer; + text-align: left; + transition: all 0.2s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.documentLink:hover { + background-color: var(--color-primary-light, #eff6ff); + border-color: var(--color-primary-dark, #2563eb); + transform: translateY(-2px); + box-shadow: 0 4px 6px rgba(59, 130, 246, 0.2); +} + +.documentLink:active { + transform: translateY(0); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.documentLabel { + font-weight: 600; + color: var(--color-primary, #3b82f6); + font-size: 1rem; + word-break: break-word; +} + +.documentLink:hover .documentLabel { + color: var(--color-primary-dark, #2563eb); +} + +.documentType { + font-size: 0.8rem; + color: var(--color-text-secondary, #6b7280); + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 500; +} + +/* BZO Information Styles */ +.bzoButtonContainer { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.bzoButton { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + background-color: var(--color-primary, #3b82f6); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + transition: all 0.2s ease; + width: fit-content; +} + +.bzoButton:hover:not(:disabled) { + background-color: var(--color-primary-dark, #2563eb); + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3); +} + +.bzoButton:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.spinner { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.errorMessage { + color: var(--color-error, #ef4444); + font-size: 0.875rem; + padding: 0.5rem; + background-color: var(--color-error-light, #fee2e2); + border-radius: 4px; + border: 1px solid var(--color-error, #ef4444); +} + +.bzoSection { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 2px solid var(--color-primary, #3b82f6); + background-color: var(--color-bg-secondary, #f9fafb); + padding: 1.5rem; + border-radius: 8px; + margin-left: -1.5rem; + margin-right: -1.5rem; +} + +.bzoHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.bzoSectionTitle { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + color: var(--color-text, #111827); +} + +.toggleButton { + background: none; + border: none; + cursor: pointer; + font-size: 1rem; + color: var(--color-text-secondary, #6b7280); + padding: 0.25rem 0.5rem; + border-radius: 4px; + transition: background-color 0.2s; +} + +.toggleButton:hover { + background-color: var(--color-hover, #f3f4f6); +} + +.bzoContent { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.bzoSubSection { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.bzoSubTitle { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: var(--color-text, #111827); + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--color-border, #e5e7eb); +} + +.bzoSummary { + padding: 1rem; + background-color: var(--color-bg, #ffffff); + border-left: 4px solid var(--color-primary, #3b82f6); + border-radius: 4px; + color: var(--color-text, #111827); +} + +/* Markdown Styles for BZO Content */ +.bzoMarkdown { + line-height: 1.6; + color: var(--color-text, #111827); +} + +.bzoMarkdownH1, +.bzoMarkdownH2, +.bzoMarkdownH3, +.bzoMarkdownH4, +.bzoMarkdownH5, +.bzoMarkdownH6 { + margin-top: 1.5rem; + margin-bottom: 0.75rem; + font-weight: 600; + color: var(--color-text, #111827); + line-height: 1.3; +} + +.bzoMarkdownH1 { + font-size: 1.5rem; + border-bottom: 2px solid var(--color-border, #e5e7eb); + padding-bottom: 0.5rem; +} + +.bzoMarkdownH2 { + font-size: 1.25rem; + border-bottom: 1px solid var(--color-border, #e5e7eb); + padding-bottom: 0.25rem; +} + +.bzoMarkdownH3 { + font-size: 1.1rem; +} + +.bzoMarkdownH4 { + font-size: 1rem; +} + +.bzoMarkdownH5 { + font-size: 0.95rem; +} + +.bzoMarkdownH6 { + font-size: 0.9rem; +} + +.bzoMarkdownP { + margin: 0.75rem 0; + line-height: 1.6; +} + +.bzoMarkdownP:first-child { + margin-top: 0; +} + +.bzoMarkdownP:last-child { + margin-bottom: 0; +} + +.bzoMarkdownUl, +.bzoMarkdownOl { + margin: 0.75rem 0; + padding-left: 1.5rem; +} + +.bzoMarkdownLi { + margin: 0.5rem 0; + line-height: 1.6; +} + +.bzoMarkdownUl .bzoMarkdownLi { + list-style-type: disc; +} + +.bzoMarkdownOl .bzoMarkdownLi { + list-style-type: decimal; +} + +.bzoMarkdownTableWrapper { + overflow-x: auto; + margin: 1rem 0; +} + +.bzoMarkdownTable { + width: 100%; + border-collapse: collapse; + border: 1px solid var(--color-border, #e5e7eb); + border-radius: 4px; + overflow: hidden; +} + +.bzoMarkdownThead { + background-color: var(--color-bg-secondary, #f9fafb); +} + +.bzoMarkdownTh { + padding: 0.75rem; + text-align: left; + font-weight: 600; + border-bottom: 2px solid var(--color-border, #e5e7eb); + color: var(--color-text, #111827); +} + +.bzoMarkdownTd { + padding: 0.75rem; + border-bottom: 1px solid var(--color-border, #e5e7eb); + color: var(--color-text, #111827); +} + +.bzoMarkdownTr:last-child .bzoMarkdownTd { + border-bottom: none; +} + +.bzoMarkdownTr:hover { + background-color: var(--color-hover, #f3f4f6); +} + +.bzoMarkdownCodeInline { + background-color: var(--color-bg-secondary, #f9fafb); + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-family: 'Courier New', monospace; + font-size: 0.9em; + color: var(--color-primary, #3b82f6); + border: 1px solid var(--color-border, #e5e7eb); +} + +.bzoMarkdownPre { + background-color: var(--color-bg-secondary, #f9fafb); + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + border: 1px solid var(--color-border, #e5e7eb); + margin: 1rem 0; +} + +.bzoMarkdownCodeBlock { + font-family: 'Courier New', monospace; + font-size: 0.9em; + color: var(--color-text, #111827); +} + +.bzoMarkdownBlockquote { + margin: 1rem 0; + padding: 0.75rem 1rem; + border-left: 4px solid var(--color-primary, #3b82f6); + background-color: var(--color-bg-secondary, #f9fafb); + border-radius: 4px; + color: var(--color-text-secondary, #6b7280); + font-style: italic; +} + +.bzoMarkdownStrong { + font-weight: 600; + color: var(--color-text, #111827); +} + +.bzoMarkdownEm { + font-style: italic; +} + +.bzoMarkdownLink { + color: var(--color-primary, #3b82f6); + text-decoration: none; + font-weight: 500; + transition: color 0.2s; +} + +.bzoMarkdownLink:hover { + color: var(--color-primary-dark, #2563eb); + text-decoration: underline; +} + +.bzoMarkdownHr { + margin: 1.5rem 0; + border: none; + border-top: 1px solid var(--color-border, #e5e7eb); +} + +.bzoInfoGrid { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.bzoInfoItem { + display: flex; + gap: 0.5rem; +} + +.bzoLabel { + font-weight: 500; + color: var(--color-text-secondary, #6b7280); + min-width: 80px; +} + +.bzoValue { + color: var(--color-text, #111827); +} + +.bzoZonesList { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.bzoZoneCard { + padding: 1rem; + background-color: var(--color-bg, #ffffff); + border: 1px solid var(--color-border, #e5e7eb); + border-radius: 6px; + transition: box-shadow 0.2s; +} + +.bzoZoneCard:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.bzoZoneHeader { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.75rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--color-border, #e5e7eb); +} + +.bzoZoneCode { + font-weight: 700; + font-size: 1.1rem; + color: var(--color-primary, #3b82f6); +} + +.bzoZoneName { + font-weight: 600; + color: var(--color-text, #111827); +} + +.bzoZoneDetails { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.bzoZoneDetailItem { + display: flex; + gap: 0.5rem; +} + +.bzoDetailLabel { + font-weight: 500; + color: var(--color-text-secondary, #6b7280); + min-width: 140px; + font-size: 0.875rem; +} + +.bzoDetailValue { + color: var(--color-text, #111827); + font-size: 0.875rem; +} + +.bzoRulesList { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.bzoRuleCard { + padding: 1rem; + background-color: var(--color-bg, #ffffff); + border: 1px solid var(--color-border, #e5e7eb); + border-left: 4px solid var(--color-primary, #3b82f6); + border-radius: 6px; + transition: box-shadow 0.2s; +} + +.bzoRuleCard:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.bzoRuleHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.bzoRuleType { + font-weight: 600; + color: var(--color-text, #111827); + text-transform: capitalize; + font-size: 0.95rem; +} + +.bzoConfidence { + font-size: 0.75rem; + color: var(--color-text-secondary, #6b7280); + background-color: var(--color-bg-secondary, #f9fafb); + padding: 0.25rem 0.5rem; + border-radius: 4px; +} + +.bzoRuleValue { + margin-bottom: 0.5rem; +} + +.bzoRuleNumeric { + font-size: 1.1rem; + font-weight: 600; + color: var(--color-primary, #3b82f6); +} + +.bzoRuleText { + color: var(--color-text, #111827); + font-weight: 500; +} + +.bzoRuleSnippet { + margin-top: 0.5rem; + padding: 0.5rem; + background-color: var(--color-bg-secondary, #f9fafb); + border-radius: 4px; + font-style: italic; + color: var(--color-text-secondary, #6b7280); + font-size: 0.875rem; +} + +.bzoRuleZone, +.bzoRulePage { + font-size: 0.75rem; + color: var(--color-text-secondary, #6b7280); + margin-top: 0.25rem; +} + +.bzoRuleMeta { + display: flex; + gap: 1rem; + margin-top: 0.5rem; + padding-top: 0.5rem; + border-top: 1px solid var(--color-border, #e5e7eb); + font-size: 0.75rem; + color: var(--color-text-secondary, #6b7280); +} + +.bzoArticlesList { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.bzoArticleCard { + padding: 1rem; + background-color: var(--color-bg, #ffffff); + border: 1px solid var(--color-border, #e5e7eb); + border-radius: 6px; + transition: box-shadow 0.2s; +} + +.bzoArticleCard:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.bzoArticleHeader { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.75rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--color-border, #e5e7eb); +} + +.bzoArticleLabel { + font-weight: 700; + color: var(--color-primary, #3b82f6); + font-size: 1rem; +} + +.bzoArticleTitle { + font-weight: 600; + color: var(--color-text, #111827); +} + +.bzoArticleText { + margin-bottom: 0.75rem; + line-height: 1.6; + color: var(--color-text, #111827); + white-space: pre-wrap; +} + +.bzoArticleMeta { + display: flex; + gap: 1rem; + font-size: 0.75rem; + color: var(--color-text-secondary, #6b7280); + padding-top: 0.5rem; + border-top: 1px solid var(--color-border, #e5e7eb); +} + +.bzoDocumentsList { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.bzoDocumentItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem; + background-color: var(--color-bg, #ffffff); + border: 1px solid var(--color-border, #e5e7eb); + border-radius: 4px; +} + +.bzoDocumentLabel { + font-weight: 500; + color: var(--color-text, #111827); +} + +.bzoDocumentType { + font-size: 0.75rem; + color: var(--color-text-secondary, #6b7280); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.bzoErrors, +.bzoWarnings { + margin-top: 1rem; +} + +.bzoErrorTitle { + margin: 0 0 0.5rem 0; + font-size: 0.95rem; + font-weight: 600; + color: var(--color-error, #ef4444); +} + +.bzoWarningTitle { + margin: 0 0 0.5rem 0; + font-size: 0.95rem; + font-weight: 600; + color: #f59e0b; +} + +.bzoErrorList, +.bzoWarningList { + margin: 0; + padding-left: 1.5rem; + color: var(--color-text, #111827); +} + +.bzoErrorList li { + color: var(--color-error, #ef4444); + margin-bottom: 0.25rem; +} + +.bzoWarningList li { + color: #f59e0b; + margin-bottom: 0.25rem; +} + +.bzoStats { + display: flex; + gap: 2rem; + flex-wrap: wrap; +} + +.bzoStatItem { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.bzoStatLabel { + font-size: 0.875rem; + color: var(--color-text-secondary, #6b7280); + font-weight: 500; +} + +.bzoStatValue { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-primary, #3b82f6); +} + @media (max-width: 768px) { .panel { width: 100vw; diff --git a/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx b/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx index 5185dca..a0fb542 100644 --- a/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx +++ b/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx @@ -1,6 +1,10 @@ -import React from 'react'; +import React, { useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { FaTimes, FaTrash } from 'react-icons/fa'; +import { FaTimes, FaTrash, FaInfoCircle, FaSpinner } from 'react-icons/fa'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { ContentPreview } from '../../ContentPreview'; +import api from '../../../api'; import styles from './ParcelInfoPanel.module.css'; export interface ParcelInfoPanelProps { @@ -11,6 +15,77 @@ export interface ParcelInfoPanelProps { adjacentParcels?: any[]; } +// BZO Information Types +interface BZOGemeinde { + id: string; + label: string; + plz: string; +} + +interface BZOZone { + zone_code: string; + zone_name: string; + zone_category: string; + geschosszahl: number; + gewerbeerleichterung: boolean; + source_article: string; + page: number; +} + +interface BZORule { + rule_type: string; + value_numeric?: number; + value_text: string; + unit?: string; + condition_text?: string | null; + is_table_rule: boolean; + table_zones: string[]; + page: number; + text_snippet: string; + zone_raw: string; + rule_scope: string; + confidence: number; +} + +interface BZOArticle { + article_label: string; + article_title: string; + text: string; + page_start: number; + page_end: number; + section_level_1?: string | null; + section_level_2?: string | null; + section_level_3?: string | null; + zone_raw: string; +} + +interface BZOExtractedContent { + zones: BZOZone[]; + rules: BZORule[]; + articles: BZOArticle[]; + total_zones: number; + total_rules: number; + total_articles: number; +} + +interface BZODocument { + id: string; + label: string; + dokumentTyp: string; +} + +export interface BZOInformationResponse { + parcel_id: string; + bauzone: string; + gemeinde: BZOGemeinde; + extracted_content: BZOExtractedContent; + ai_summary: string; + relevant_rules: BZORule[]; + documents_processed: BZODocument[]; + errors: string[]; + warnings: string[]; +} + const ParcelInfoPanel: React.FC = ({ isOpen, onClose, @@ -18,256 +93,721 @@ const ParcelInfoPanel: React.FC = ({ onRemoveParcel, adjacentParcels = [] }) => { + // State for document preview popup + const [previewDocument, setPreviewDocument] = useState<{ + fileId: string; + fileName: string; + mimeType: string; + } | null>(null); + + // State for BZO information + const [bzoInfo, setBzoInfo] = useState>({}); + const [loadingBzo, setLoadingBzo] = useState>({}); + const [bzoError, setBzoError] = useState>({}); + const [expandedBzo, setExpandedBzo] = useState>({}); + + // Fetch BZO information + const fetchBZOInformation = async (parcelData: any) => { + // Extract gemeinde and bauzone from parcel data + const gemeinde = parcelData.gemeinde?.label || parcelData.parcel?.municipality_name; + const bauzone = parcelData.parcel?.bauzone; + + // Use parcel ID as key for state management + const parcelKey = parcelData.parcel?.id || parcelData.parcel?.number || 'unknown'; + + // Validate required fields + if (!gemeinde) { + const errorMsg = 'Gemeinde-Information fehlt. BZO-Informationen können nicht abgerufen werden.'; + setBzoError(prev => ({ ...prev, [parcelKey]: errorMsg })); + return; + } + + if (!bauzone) { + const errorMsg = 'Bauzone-Information fehlt. BZO-Informationen können nicht abgerufen werden.'; + setBzoError(prev => ({ ...prev, [parcelKey]: errorMsg })); + return; + } + + if (loadingBzo[parcelKey] || bzoInfo[parcelKey]) return; // Already loading or loaded + + setLoadingBzo(prev => ({ ...prev, [parcelKey]: true })); + setBzoError(prev => ({ ...prev, [parcelKey]: '' })); + + try { + const response = await api.get( + '/api/realestate/bzo-information', + { + params: { + gemeinde: gemeinde, + bauzone: bauzone + } + } + ); + setBzoInfo(prev => ({ ...prev, [parcelKey]: response.data })); + setExpandedBzo(prev => ({ ...prev, [parcelKey]: true })); + } catch (error: any) { + const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || 'Fehler beim Laden der BZO-Informationen'; + setBzoError(prev => ({ ...prev, [parcelKey]: errorMessage })); + console.error('Error fetching BZO information:', error); + } finally { + setLoadingBzo(prev => ({ ...prev, [parcelKey]: false })); + } + }; + + const toggleBZOExpanded = (parcelId: string) => { + setExpandedBzo(prev => ({ ...prev, [parcelId]: !prev[parcelId] })); + }; + if (!parcels || parcels.length === 0) return null; return ( - - {isOpen && ( - <> - {/* Backdrop */} - - - {/* Panel */} - -
-

Parzellen-Informationen ({parcels.length})

- -
- -
- {/* Selected Parcels List */} -
- {parcels.map((parcelData, index) => ( -
-
-

- Parzelle {index + 1}: {parcelData.parcel.number || parcelData.parcel.id || 'Unbekannt'} -

- {onRemoveParcel && ( - - )} -
-
- {parcelData.parcel.id && ( -
- ID: - {parcelData.parcel.id} -
- )} - {parcelData.parcel.number && ( -
- Nummer: - {parcelData.parcel.number} -
- )} - {parcelData.parcel.name && ( -
- Name: - {parcelData.parcel.name} -
- )} - {parcelData.parcel.egrid && ( -
- EGRID: - {parcelData.parcel.egrid} -
- )} - {parcelData.parcel.identnd && ( -
- IdentND: - {parcelData.parcel.identnd} -
- )} - {parcelData.parcel.address && ( -
- Adresse: - {parcelData.parcel.address} -
- )} - {parcelData.parcel.canton && ( -
- Kanton: - {parcelData.parcel.canton} -
- )} - {parcelData.parcel.municipality_name && ( -
- Gemeinde: - {parcelData.parcel.municipality_name} -
- )} - {parcelData.parcel.municipality_code && ( -
- Gemeinde-Code: - {parcelData.parcel.municipality_code} -
- )} - {parcelData.parcel.area_m2 !== undefined && ( -
- Fläche: - - {parcelData.parcel.area_m2.toFixed(2)} m² - {parcelData.parcel.area_m2 >= 10000 && ( - - {' '}({(parcelData.parcel.area_m2 / 10000).toFixed(2)} ha) - - )} - -
- )} - {parcelData.parcel.realestate_type && ( -
- Grundstückstyp: - {parcelData.parcel.realestate_type} -
- )} - {parcelData.parcel.bauzone && ( -
- Bauzone: - {parcelData.parcel.bauzone} -
- )} - {parcelData.parcel.zone && Array.isArray(parcelData.parcel.zone) && parcelData.parcel.zone.length > 0 && ( -
- Zone: - - {parcelData.parcel.zone.length} Zone{parcelData.parcel.zone.length !== 1 ? 'n' : ''} gefunden - {(() => { - // Extract zone types from zone array - const zoneTypes = parcelData.parcel.zone - .map((z: any) => { - const attrs = z.attributes || {}; - return attrs.typ || attrs.zone_typ || attrs.bauzone || attrs.zone || attrs.label || null; - }) - .filter((t: string | null) => t !== null); - - if (zoneTypes.length > 0) { - return ( - - {' '}({zoneTypes.join(', ')}) - - ); - } - return null; - })()} - {import.meta.env.DEV && ( -
- Details anzeigen -
-                                  {JSON.stringify(parcelData.parcel.zone, null, 2)}
-                                
-
- )} -
-
- )} - {parcelData.parcel.centroid && ( -
- Zentrum (LV95): - - {parcelData.parcel.centroid.x.toFixed(2)}, {parcelData.parcel.centroid.y.toFixed(2)} - -
- )} - {parcelData.parcel.geoportal_url && ( -
- Geoportal: - - Link öffnen - -
- )} -
- - {/* Map View Info for this parcel */} - {parcelData.map_view && ( -
-

Kartenansicht

-
- {parcelData.map_view.center && ( -
- Zentrum: - - {parcelData.map_view.center.x.toFixed(2)}, {parcelData.map_view.center.y.toFixed(2)} - -
- )} - {parcelData.map_view.zoom_bounds && ( - <> -
- Bounds Min: - - {parcelData.map_view.zoom_bounds.min_x.toFixed(2)}, {parcelData.map_view.zoom_bounds.min_y.toFixed(2)} - -
-
- Bounds Max: - - {parcelData.map_view.zoom_bounds.max_x.toFixed(2)}, {parcelData.map_view.zoom_bounds.max_y.toFixed(2)} - -
- - )} -
-
- )} -
- ))} + <> + + {isOpen && ( + <> + {/* Backdrop */} + + + {/* Panel */} + +
+

Parzellen-Informationen ({parcels.length})

+
- {/* Adjacent Parcels */} - {adjacentParcels.length > 0 && ( -
-

- Angrenzende Parzellen ({adjacentParcels.length}) -

-
- {adjacentParcels.map((adjacent, index) => ( -
-
- - {adjacent.number || adjacent.id} - - {adjacent.egrid && ( - {adjacent.egrid} - )} -
+
+ {/* Selected Parcels List */} +
+ {parcels.map((parcelData, index) => ( +
+
+

+ Parzelle {index + 1}: {parcelData.parcel.number || parcelData.parcel.id || 'Unbekannt'} +

+ {onRemoveParcel && ( + + )}
- ))} -
-
- )} -
- - +
+ {parcelData.parcel.id && ( +
+ ID: + {parcelData.parcel.id} +
+ )} + {parcelData.parcel.number && ( +
+ Nummer: + {parcelData.parcel.number} +
+ )} + {parcelData.parcel.name && ( +
+ Name: + {parcelData.parcel.name} +
+ )} + {parcelData.parcel.egrid && ( +
+ EGRID: + {parcelData.parcel.egrid} +
+ )} + {parcelData.parcel.identnd && ( +
+ IdentND: + {parcelData.parcel.identnd} +
+ )} + {parcelData.parcel.address && ( +
+ Adresse: + {parcelData.parcel.address} +
+ )} + {parcelData.parcel.canton && ( +
+ Kanton: + {parcelData.parcel.canton} +
+ )} + {parcelData.parcel.municipality_name && ( +
+ Gemeinde: + {parcelData.parcel.municipality_name} +
+ )} + {parcelData.parcel.municipality_code && ( +
+ Gemeinde-Code: + {parcelData.parcel.municipality_code} +
+ )} + {parcelData.parcel.area_m2 !== undefined && ( +
+ Fläche: + + {parcelData.parcel.area_m2.toFixed(2)} m² + {parcelData.parcel.area_m2 >= 10000 && ( + + {' '}({(parcelData.parcel.area_m2 / 10000).toFixed(2)} ha) + + )} + +
+ )} + {parcelData.parcel.realestate_type && ( +
+ Grundstückstyp: + {parcelData.parcel.realestate_type} +
+ )} + {parcelData.parcel.bauzone && ( +
+ Bauzone: + {parcelData.parcel.bauzone} +
+ )} + {parcelData.parcel.id && ( +
+ BZO-Informationen: +
+ {(() => { + const parcelKey = parcelData.parcel.id || parcelData.parcel.number || 'unknown'; + return ( + <> + {!bzoInfo[parcelKey] && !loadingBzo[parcelKey] && ( + + )} + {loadingBzo[parcelKey] && ( + + )} + {bzoError[parcelKey] && ( +
+ {bzoError[parcelKey]} +
+ )} + + ); + })()} +
+
+ )} + {parcelData.parcel.zone && Array.isArray(parcelData.parcel.zone) && parcelData.parcel.zone.length > 0 && ( +
+ Zone: + + {parcelData.parcel.zone.length} Zone{parcelData.parcel.zone.length !== 1 ? 'n' : ''} gefunden + {(() => { + // Extract zone types from zone array + const zoneTypes = parcelData.parcel.zone + .map((z: any) => { + const attrs = z.attributes || {}; + return attrs.typ || attrs.zone_typ || attrs.bauzone || attrs.zone || attrs.label || null; + }) + .filter((t: string | null) => t !== null); + + if (zoneTypes.length > 0) { + return ( + + {' '}({zoneTypes.join(', ')}) + + ); + } + return null; + })()} + {import.meta.env.DEV && ( +
+ Details anzeigen +
+                                    {JSON.stringify(parcelData.parcel.zone, null, 2)}
+                                  
+
+ )} +
+
+ )} + {parcelData.parcel.centroid && ( +
+ Zentrum (LV95): + + {parcelData.parcel.centroid.x.toFixed(2)}, {parcelData.parcel.centroid.y.toFixed(2)} + +
+ )} + {parcelData.parcel.geoportal_url && ( +
+ Geoportal: + + Link öffnen + +
+ )} + {parcelData.gemeinde && ( +
+ Gemeinde: + + {parcelData.gemeinde.label} + {parcelData.gemeinde.plz && ` (${parcelData.gemeinde.plz})`} + +
+ )} +
+ + {/* BZO Information Section */} + {(() => { + const parcelKey = parcelData.parcel.id || parcelData.parcel.number || 'unknown'; + return bzoInfo[parcelKey] && ( +
+
+

BZO-Informationen

+ +
+ {expandedBzo[parcelKey] && ( + + )} +
+ ); + })()} + + {/* Documents Section */} + {parcelData.documents && Array.isArray(parcelData.documents) && parcelData.documents.length > 0 && ( +
+

Dokumente ({parcelData.documents.length})

+
+ {parcelData.documents.map((document) => ( + + ))} +
+
+ )} + + ))} +
+ + {/* Adjacent Parcels */} + {adjacentParcels.length > 0 && ( +
+

+ Angrenzende Parzellen ({adjacentParcels.length}) +

+
+ {adjacentParcels.map((adjacent, index) => ( +
+
+ + {adjacent.number || adjacent.id} + + {adjacent.egrid && ( + {adjacent.egrid} + )} +
+
+ ))} +
+
+ )} + +
+ + )} +
+ + {/* Document Preview Popup */} + {previewDocument && ( + setPreviewDocument(null)} + fileId={previewDocument.fileId} + fileName={previewDocument.fileName} + mimeType={previewDocument.mimeType} + /> )} - + + ); +}; + +// BZO Information Display Component +interface BZOInformationDisplayProps { + data: BZOInformationResponse; +} + +const BZOInformationDisplay: React.FC = ({ data }) => { + return ( +
+ {/* Summary Section */} + {data.ai_summary && ( +
+
Zusammenfassung
+
+

, + h2: ({node, ...props}) =>

, + h3: ({node, ...props}) =>

, + h4: ({node, ...props}) =>

, + h5: ({node, ...props}) =>

, + h6: ({node, ...props}) =>
, + p: ({node, ...props}) =>

, + ul: ({node, ...props}) =>