feat:extended BZO information

This commit is contained in:
Ida Dittrich 2026-01-27 14:29:30 +01:00
parent b142b93666
commit 1f87e00339
3 changed files with 1480 additions and 245 deletions

View file

@ -125,6 +125,24 @@
color: var(--color-text-secondary, #6b7280); 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 { .infoGrid {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -241,6 +259,670 @@
overflow-y: auto; 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) { @media (max-width: 768px) {
.panel { .panel {
width: 100vw; width: 100vw;

View file

@ -1,6 +1,10 @@
import React from 'react'; import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; 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'; import styles from './ParcelInfoPanel.module.css';
export interface ParcelInfoPanelProps { export interface ParcelInfoPanelProps {
@ -11,6 +15,77 @@ export interface ParcelInfoPanelProps {
adjacentParcels?: any[]; 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<ParcelInfoPanelProps> = ({ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
isOpen, isOpen,
onClose, onClose,
@ -18,9 +93,75 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
onRemoveParcel, onRemoveParcel,
adjacentParcels = [] 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<Record<string, BZOInformationResponse | null>>({});
const [loadingBzo, setLoadingBzo] = useState<Record<string, boolean>>({});
const [bzoError, setBzoError] = useState<Record<string, string>>({});
const [expandedBzo, setExpandedBzo] = useState<Record<string, boolean>>({});
// 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<BZOInformationResponse>(
'/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; if (!parcels || parcels.length === 0) return null;
return ( return (
<>
<AnimatePresence> <AnimatePresence>
{isOpen && ( {isOpen && (
<> <>
@ -148,6 +289,39 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
<span className={styles.value}>{parcelData.parcel.bauzone}</span> <span className={styles.value}>{parcelData.parcel.bauzone}</span>
</div> </div>
)} )}
{parcelData.parcel.id && (
<div className={styles.infoItem}>
<span className={styles.label}>BZO-Informationen:</span>
<div className={styles.bzoButtonContainer}>
{(() => {
const parcelKey = parcelData.parcel.id || parcelData.parcel.number || 'unknown';
return (
<>
{!bzoInfo[parcelKey] && !loadingBzo[parcelKey] && (
<button
className={styles.bzoButton}
onClick={() => fetchBZOInformation(parcelData)}
title="BZO-Informationen abrufen"
>
<FaInfoCircle /> BZO-Informationen laden
</button>
)}
{loadingBzo[parcelKey] && (
<button className={styles.bzoButton} disabled>
<FaSpinner className={styles.spinner} /> Lädt...
</button>
)}
{bzoError[parcelKey] && (
<div className={styles.errorMessage}>
{bzoError[parcelKey]}
</div>
)}
</>
);
})()}
</div>
</div>
)}
{parcelData.parcel.zone && Array.isArray(parcelData.parcel.zone) && parcelData.parcel.zone.length > 0 && ( {parcelData.parcel.zone && Array.isArray(parcelData.parcel.zone) && parcelData.parcel.zone.length > 0 && (
<div className={styles.infoItem}> <div className={styles.infoItem}>
<span className={styles.label}>Zone:</span> <span className={styles.label}>Zone:</span>
@ -203,37 +377,62 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
</a> </a>
</div> </div>
)} )}
{parcelData.gemeinde && (
<div className={styles.infoItem}>
<span className={styles.label}>Gemeinde:</span>
<span className={styles.value}>
{parcelData.gemeinde.label}
{parcelData.gemeinde.plz && ` (${parcelData.gemeinde.plz})`}
</span>
</div>
)}
</div> </div>
{/* Map View Info for this parcel */} {/* BZO Information Section */}
{parcelData.map_view && ( {(() => {
<div className={styles.mapViewSection}> const parcelKey = parcelData.parcel.id || parcelData.parcel.number || 'unknown';
<h4 className={styles.subSectionTitle}>Kartenansicht</h4> return bzoInfo[parcelKey] && (
<div className={styles.infoGrid}> <div className={styles.bzoSection}>
{parcelData.map_view.center && ( <div className={styles.bzoHeader}>
<div className={styles.infoItem}> <h4 className={styles.bzoSectionTitle}>BZO-Informationen</h4>
<span className={styles.label}>Zentrum:</span> <button
<span className={styles.value}> className={styles.toggleButton}
{parcelData.map_view.center.x.toFixed(2)}, {parcelData.map_view.center.y.toFixed(2)} onClick={() => toggleBZOExpanded(parcelKey)}
</span> >
{expandedBzo[parcelKey] ? '▼' : '▶'}
</button>
</div> </div>
{expandedBzo[parcelKey] && (
<BZOInformationDisplay data={bzoInfo[parcelKey]!} />
)} )}
{parcelData.map_view.zoom_bounds && (
<>
<div className={styles.infoItem}>
<span className={styles.label}>Bounds Min:</span>
<span className={styles.value}>
{parcelData.map_view.zoom_bounds.min_x.toFixed(2)}, {parcelData.map_view.zoom_bounds.min_y.toFixed(2)}
</span>
</div> </div>
<div className={styles.infoItem}> );
<span className={styles.label}>Bounds Max:</span> })()}
<span className={styles.value}>
{parcelData.map_view.zoom_bounds.max_x.toFixed(2)}, {parcelData.map_view.zoom_bounds.max_y.toFixed(2)} {/* Documents Section */}
</span> {parcelData.documents && Array.isArray(parcelData.documents) && parcelData.documents.length > 0 && (
</div> <div className={styles.documentsSection}>
</> <h4 className={styles.documentsSectionTitle}>Dokumente ({parcelData.documents.length})</h4>
<div className={styles.documentsList}>
{parcelData.documents.map((document) => (
<button
key={document.id}
className={styles.documentLink}
onClick={() => {
setPreviewDocument({
fileId: document.dokumentReferenz,
fileName: document.label,
mimeType: document.mimeType
});
}}
title={`${document.label} (${document.dokumentTyp})`}
>
<span className={styles.documentLabel}>{document.label}</span>
{document.dokumentTyp && (
<span className={styles.documentType}>{document.dokumentTyp}</span>
)} )}
</button>
))}
</div> </div>
</div> </div>
)} )}
@ -268,6 +467,347 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
</> </>
)} )}
</AnimatePresence> </AnimatePresence>
{/* Document Preview Popup */}
{previewDocument && (
<ContentPreview
isOpen={!!previewDocument}
onClose={() => setPreviewDocument(null)}
fileId={previewDocument.fileId}
fileName={previewDocument.fileName}
mimeType={previewDocument.mimeType}
/>
)}
</>
);
};
// BZO Information Display Component
interface BZOInformationDisplayProps {
data: BZOInformationResponse;
}
const BZOInformationDisplay: React.FC<BZOInformationDisplayProps> = ({ data }) => {
return (
<div className={styles.bzoContent}>
{/* Summary Section */}
{data.ai_summary && (
<div className={styles.bzoSubSection}>
<h5 className={styles.bzoSubTitle}>Zusammenfassung</h5>
<div className={styles.bzoSummary}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
className={styles.bzoMarkdown}
components={{
h1: ({node, ...props}) => <h1 className={styles.bzoMarkdownH1} {...props} />,
h2: ({node, ...props}) => <h2 className={styles.bzoMarkdownH2} {...props} />,
h3: ({node, ...props}) => <h3 className={styles.bzoMarkdownH3} {...props} />,
h4: ({node, ...props}) => <h4 className={styles.bzoMarkdownH4} {...props} />,
h5: ({node, ...props}) => <h5 className={styles.bzoMarkdownH5} {...props} />,
h6: ({node, ...props}) => <h6 className={styles.bzoMarkdownH6} {...props} />,
p: ({node, ...props}) => <p className={styles.bzoMarkdownP} {...props} />,
ul: ({node, ...props}) => <ul className={styles.bzoMarkdownUl} {...props} />,
ol: ({node, ...props}) => <ol className={styles.bzoMarkdownOl} {...props} />,
li: ({node, ...props}) => <li className={styles.bzoMarkdownLi} {...props} />,
table: ({node, ...props}) => <div className={styles.bzoMarkdownTableWrapper}><table className={styles.bzoMarkdownTable} {...props} /></div>,
thead: ({node, ...props}) => <thead className={styles.bzoMarkdownThead} {...props} />,
tbody: ({node, ...props}) => <tbody className={styles.bzoMarkdownTbody} {...props} />,
tr: ({node, ...props}) => <tr className={styles.bzoMarkdownTr} {...props} />,
th: ({node, ...props}) => <th className={styles.bzoMarkdownTh} {...props} />,
td: ({node, ...props}) => <td className={styles.bzoMarkdownTd} {...props} />,
code: ({node, inline, ...props}: any) =>
inline ? (
<code className={styles.bzoMarkdownCodeInline} {...props} />
) : (
<code className={styles.bzoMarkdownCodeBlock} {...props} />
),
pre: ({node, ...props}) => <pre className={styles.bzoMarkdownPre} {...props} />,
blockquote: ({node, ...props}) => <blockquote className={styles.bzoMarkdownBlockquote} {...props} />,
strong: ({node, ...props}) => <strong className={styles.bzoMarkdownStrong} {...props} />,
em: ({node, ...props}) => <em className={styles.bzoMarkdownEm} {...props} />,
a: ({node, ...props}: any) => <a className={styles.bzoMarkdownLink} {...props} />,
hr: ({node, ...props}) => <hr className={styles.bzoMarkdownHr} {...props} />,
}}
>
{data.ai_summary}
</ReactMarkdown>
</div>
</div>
)}
{/* Gemeinde Info */}
{data.gemeinde && (
<div className={styles.bzoSubSection}>
<h5 className={styles.bzoSubTitle}>Gemeinde</h5>
<div className={styles.bzoInfoGrid}>
<div className={styles.bzoInfoItem}>
<span className={styles.bzoLabel}>Name:</span>
<span className={styles.bzoValue}>{data.gemeinde.label}</span>
</div>
{data.gemeinde.plz && (
<div className={styles.bzoInfoItem}>
<span className={styles.bzoLabel}>PLZ:</span>
<span className={styles.bzoValue}>{data.gemeinde.plz}</span>
</div>
)}
</div>
</div>
)}
{/* Zones */}
{data.extracted_content?.zones && data.extracted_content.zones.length > 0 && (
<div className={styles.bzoSubSection}>
<h5 className={styles.bzoSubTitle}>
Zonen ({data.extracted_content.zones.length})
</h5>
<div className={styles.bzoZonesList}>
{data.extracted_content.zones.map((zone, idx) => (
<div key={idx} className={styles.bzoZoneCard}>
<div className={styles.bzoZoneHeader}>
<span className={styles.bzoZoneCode}>{zone.zone_code}</span>
<span className={styles.bzoZoneName}>{zone.zone_name}</span>
</div>
<div className={styles.bzoZoneDetails}>
<div className={styles.bzoZoneDetailItem}>
<span className={styles.bzoDetailLabel}>Kategorie:</span>
<span className={styles.bzoDetailValue}>{zone.zone_category}</span>
</div>
{zone.geschosszahl !== undefined && (
<div className={styles.bzoZoneDetailItem}>
<span className={styles.bzoDetailLabel}>Geschosszahl:</span>
<span className={styles.bzoDetailValue}>{zone.geschosszahl}</span>
</div>
)}
<div className={styles.bzoZoneDetailItem}>
<span className={styles.bzoDetailLabel}>Gewerbeerleichterung:</span>
<span className={styles.bzoDetailValue}>
{zone.gewerbeerleichterung ? 'Ja' : 'Nein'}
</span>
</div>
{zone.source_article && (
<div className={styles.bzoZoneDetailItem}>
<span className={styles.bzoDetailLabel}>Quelle:</span>
<span className={styles.bzoDetailValue}>
{zone.source_article} (Seite {zone.page})
</span>
</div>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* Relevant Rules */}
{data.relevant_rules && data.relevant_rules.length > 0 && (
<div className={styles.bzoSubSection}>
<h5 className={styles.bzoSubTitle}>
Relevante Regeln ({data.relevant_rules.length})
</h5>
<div className={styles.bzoRulesList}>
{data.relevant_rules.map((rule, idx) => (
<div key={idx} className={styles.bzoRuleCard}>
<div className={styles.bzoRuleHeader}>
<span className={styles.bzoRuleType}>{rule.rule_type}</span>
{rule.confidence && (
<span className={styles.bzoConfidence}>
{Math.round(rule.confidence * 100)}% Vertrauen
</span>
)}
</div>
<div className={styles.bzoRuleValue}>
{rule.value_numeric !== undefined && rule.unit ? (
<span className={styles.bzoRuleNumeric}>
{rule.value_numeric} {rule.unit}
</span>
) : (
<span className={styles.bzoRuleText}>{rule.value_text}</span>
)}
</div>
{rule.zone_raw && (
<div className={styles.bzoRuleZone}>
Zone: {rule.zone_raw}
</div>
)}
{rule.page && (
<div className={styles.bzoRulePage}>
Seite {rule.page}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* All Rules */}
{data.extracted_content?.rules && data.extracted_content.rules.length > 0 && (
<div className={styles.bzoSubSection}>
<h5 className={styles.bzoSubTitle}>
Alle Regeln ({data.extracted_content.rules.length})
</h5>
<div className={styles.bzoRulesList}>
{data.extracted_content.rules.map((rule, idx) => (
<div key={idx} className={styles.bzoRuleCard}>
<div className={styles.bzoRuleHeader}>
<span className={styles.bzoRuleType}>{rule.rule_type}</span>
{rule.confidence && (
<span className={styles.bzoConfidence}>
{Math.round(rule.confidence * 100)}%
</span>
)}
</div>
<div className={styles.bzoRuleValue}>
{rule.value_numeric !== undefined && rule.unit ? (
<span className={styles.bzoRuleNumeric}>
{rule.value_numeric} {rule.unit}
</span>
) : (
<span className={styles.bzoRuleText}>{rule.value_text}</span>
)}
</div>
{rule.text_snippet && (
<div className={styles.bzoRuleSnippet}>
"{rule.text_snippet}"
</div>
)}
<div className={styles.bzoRuleMeta}>
{rule.zone_raw && <span>Zone: {rule.zone_raw}</span>}
{rule.page && <span>Seite {rule.page}</span>}
{rule.rule_scope && <span>Bereich: {rule.rule_scope}</span>}
</div>
</div>
))}
</div>
</div>
)}
{/* Articles */}
{data.extracted_content?.articles && data.extracted_content.articles.length > 0 && (
<div className={styles.bzoSubSection}>
<h5 className={styles.bzoSubTitle}>
Artikel ({data.extracted_content.articles.length})
</h5>
<div className={styles.bzoArticlesList}>
{data.extracted_content.articles.map((article, idx) => (
<div key={idx} className={styles.bzoArticleCard}>
<div className={styles.bzoArticleHeader}>
<span className={styles.bzoArticleLabel}>{article.article_label}</span>
<span className={styles.bzoArticleTitle}>{article.article_title}</span>
</div>
<div className={styles.bzoArticleText}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
className={styles.bzoMarkdown}
components={{
h1: ({node, ...props}) => <h1 className={styles.bzoMarkdownH1} {...props} />,
h2: ({node, ...props}) => <h2 className={styles.bzoMarkdownH2} {...props} />,
h3: ({node, ...props}) => <h3 className={styles.bzoMarkdownH3} {...props} />,
h4: ({node, ...props}) => <h4 className={styles.bzoMarkdownH4} {...props} />,
h5: ({node, ...props}) => <h5 className={styles.bzoMarkdownH5} {...props} />,
h6: ({node, ...props}) => <h6 className={styles.bzoMarkdownH6} {...props} />,
p: ({node, ...props}) => <p className={styles.bzoMarkdownP} {...props} />,
ul: ({node, ...props}) => <ul className={styles.bzoMarkdownUl} {...props} />,
ol: ({node, ...props}) => <ol className={styles.bzoMarkdownOl} {...props} />,
li: ({node, ...props}) => <li className={styles.bzoMarkdownLi} {...props} />,
table: ({node, ...props}) => <div className={styles.bzoMarkdownTableWrapper}><table className={styles.bzoMarkdownTable} {...props} /></div>,
thead: ({node, ...props}) => <thead className={styles.bzoMarkdownThead} {...props} />,
tbody: ({node, ...props}) => <tbody className={styles.bzoMarkdownTbody} {...props} />,
tr: ({node, ...props}) => <tr className={styles.bzoMarkdownTr} {...props} />,
th: ({node, ...props}) => <th className={styles.bzoMarkdownTh} {...props} />,
td: ({node, ...props}) => <td className={styles.bzoMarkdownTd} {...props} />,
code: ({node, inline, ...props}: any) =>
inline ? (
<code className={styles.bzoMarkdownCodeInline} {...props} />
) : (
<code className={styles.bzoMarkdownCodeBlock} {...props} />
),
pre: ({node, ...props}) => <pre className={styles.bzoMarkdownPre} {...props} />,
blockquote: ({node, ...props}) => <blockquote className={styles.bzoMarkdownBlockquote} {...props} />,
strong: ({node, ...props}) => <strong className={styles.bzoMarkdownStrong} {...props} />,
em: ({node, ...props}) => <em className={styles.bzoMarkdownEm} {...props} />,
a: ({node, ...props}: any) => <a className={styles.bzoMarkdownLink} {...props} />,
hr: ({node, ...props}) => <hr className={styles.bzoMarkdownHr} {...props} />,
}}
>
{article.text}
</ReactMarkdown>
</div>
<div className={styles.bzoArticleMeta}>
{article.zone_raw && <span>Zone: {article.zone_raw}</span>}
<span>Seiten {article.page_start}-{article.page_end}</span>
</div>
</div>
))}
</div>
</div>
)}
{/* Documents Processed */}
{data.documents_processed && data.documents_processed.length > 0 && (
<div className={styles.bzoSubSection}>
<h5 className={styles.bzoSubTitle}>
Verarbeitete Dokumente ({data.documents_processed.length})
</h5>
<div className={styles.bzoDocumentsList}>
{data.documents_processed.map((doc, idx) => (
<div key={idx} className={styles.bzoDocumentItem}>
<span className={styles.bzoDocumentLabel}>{doc.label}</span>
<span className={styles.bzoDocumentType}>{doc.dokumentTyp}</span>
</div>
))}
</div>
</div>
)}
{/* Errors and Warnings */}
{(data.errors?.length > 0 || data.warnings?.length > 0) && (
<div className={styles.bzoSubSection}>
{(data.errors?.length > 0) && (
<div className={styles.bzoErrors}>
<h5 className={styles.bzoErrorTitle}>Fehler ({data.errors.length})</h5>
<ul className={styles.bzoErrorList}>
{data.errors.map((error, idx) => (
<li key={idx}>{error}</li>
))}
</ul>
</div>
)}
{(data.warnings?.length > 0) && (
<div className={styles.bzoWarnings}>
<h5 className={styles.bzoWarningTitle}>Warnungen ({data.warnings.length})</h5>
<ul className={styles.bzoWarningList}>
{data.warnings.map((warning, idx) => (
<li key={idx}>{warning}</li>
))}
</ul>
</div>
)}
</div>
)}
{/* Statistics */}
{data.extracted_content && (
<div className={styles.bzoSubSection}>
<h5 className={styles.bzoSubTitle}>Statistiken</h5>
<div className={styles.bzoStats}>
<div className={styles.bzoStatItem}>
<span className={styles.bzoStatLabel}>Zonen:</span>
<span className={styles.bzoStatValue}>{data.extracted_content.total_zones}</span>
</div>
<div className={styles.bzoStatItem}>
<span className={styles.bzoStatLabel}>Regeln:</span>
<span className={styles.bzoStatValue}>{data.extracted_content.total_rules}</span>
</div>
<div className={styles.bzoStatItem}>
<span className={styles.bzoStatLabel}>Artikel:</span>
<span className={styles.bzoStatValue}>{data.extracted_content.total_articles}</span>
</div>
</div>
</div>
)}
</div>
); );
}; };

View file

@ -78,6 +78,19 @@ export interface ParcelSearchResponse {
}; };
}; };
}>; }>;
gemeinde?: {
id: string;
label: string;
plz: string;
};
documents?: Array<{
id: string;
label: string;
dokumentTyp: string;
dokumentReferenz: string;
quelle: string;
mimeType: string;
}>;
} }
// Command response interface // Command response interface