895 lines
30 KiB
TypeScript
895 lines
30 KiB
TypeScript
import { useState, useCallback, useEffect } from 'react';
|
|
import api from '../api';
|
|
import type { MapPoint, ParcelGeometry } from '../components/UiComponents/MapView';
|
|
import { wgs84ToLV95 } from '../components/UiComponents/MapView/LV95Converter';
|
|
|
|
// Parcel search response interfaces
|
|
export interface ParcelSearchResponse {
|
|
parcel: {
|
|
id: string;
|
|
egrid?: string;
|
|
number?: string;
|
|
name?: string;
|
|
identnd?: string;
|
|
canton?: string;
|
|
municipality_code?: number;
|
|
municipality_name?: string;
|
|
address?: string;
|
|
perimeter?: {
|
|
closed: boolean;
|
|
punkte: Array<{
|
|
koordinatensystem: string;
|
|
x: number;
|
|
y: number;
|
|
z: number | null;
|
|
}>;
|
|
};
|
|
area_m2?: number;
|
|
centroid?: { x: number; y: number };
|
|
geoportal_url?: string;
|
|
realestate_type?: string | null;
|
|
bauzone?: string | null;
|
|
zone?: Array<any> | null;
|
|
};
|
|
map_view: {
|
|
center: { x: number; y: number };
|
|
zoom_bounds: {
|
|
min_x: number;
|
|
min_y: number;
|
|
max_x: number;
|
|
max_y: number;
|
|
};
|
|
geometry_geojson: {
|
|
type: string;
|
|
geometry: {
|
|
type: string;
|
|
coordinates: number[][][];
|
|
};
|
|
properties: {
|
|
id: string;
|
|
egrid?: string;
|
|
number?: string;
|
|
};
|
|
};
|
|
};
|
|
adjacent_parcels?: Array<{
|
|
id: string;
|
|
egrid?: string;
|
|
number?: string;
|
|
perimeter?: {
|
|
closed: boolean;
|
|
punkte: Array<{
|
|
koordinatensystem: string;
|
|
x: number;
|
|
y: number;
|
|
z: number | null;
|
|
}>;
|
|
};
|
|
geometry_geojson?: {
|
|
type: string;
|
|
geometry: {
|
|
type: string;
|
|
coordinates: number[][][];
|
|
};
|
|
properties: {
|
|
id: string;
|
|
egrid?: string;
|
|
number?: string;
|
|
};
|
|
};
|
|
}>;
|
|
gemeinde?: {
|
|
id: string;
|
|
label: string;
|
|
plz: string;
|
|
};
|
|
documents?: Array<{
|
|
id: string;
|
|
label: string;
|
|
dokumentTyp: string;
|
|
dokumentReferenz: string;
|
|
quelle: string;
|
|
mimeType: string;
|
|
}>;
|
|
}
|
|
|
|
// Command response interface
|
|
export interface CommandResponse {
|
|
success: boolean;
|
|
intent?: string;
|
|
entity?: string;
|
|
result?: any;
|
|
error?: string;
|
|
}
|
|
|
|
// Project interfaces
|
|
export interface Projekt {
|
|
id: string;
|
|
mandateId?: string;
|
|
label: string;
|
|
statusProzess?: string;
|
|
perimeter?: any;
|
|
baulinie?: any;
|
|
parzellen?: any[];
|
|
dokumente?: any[];
|
|
kontextInformationen?: any[];
|
|
}
|
|
|
|
export interface CreateProjektResponse {
|
|
projekt: Projekt;
|
|
parzellen?: any[];
|
|
}
|
|
|
|
export interface AddParcelResponse {
|
|
projekt: Projekt;
|
|
parzelle: any;
|
|
}
|
|
|
|
// Main PEK hook
|
|
export function usePek(instanceId?: string) {
|
|
// Location input state - separate fields
|
|
const [kanton, setKanton] = useState<string>('');
|
|
const [gemeinde, setGemeinde] = useState<string>('');
|
|
const [adresse, setAdresse] = useState<string>('');
|
|
const [isGettingLocation, setIsGettingLocation] = useState(false);
|
|
const [locationError, setLocationError] = useState<string | null>(null);
|
|
|
|
// Legacy locationInput for backward compatibility (combines fields)
|
|
const locationInput = [kanton, gemeinde, adresse].filter(Boolean).join(', ');
|
|
const setLocationInput = (value: string) => {
|
|
// Parse combined input if needed (for map clicks, etc.)
|
|
const parts = value.split(',').map(p => p.trim());
|
|
if (parts.length >= 3) {
|
|
setKanton(parts[0]);
|
|
setGemeinde(parts[1]);
|
|
setAdresse(parts.slice(2).join(', '));
|
|
} else {
|
|
setAdresse(value);
|
|
}
|
|
};
|
|
|
|
// Parcel search state
|
|
const [selectedParcels, setSelectedParcels] = useState<ParcelSearchResponse[]>([]);
|
|
const [isSearchingParcel, setIsSearchingParcel] = useState(false);
|
|
const [parcelSearchError, setParcelSearchError] = useState<string | null>(null);
|
|
|
|
// Map view state
|
|
const [mapCenter, setMapCenter] = useState<MapPoint | null>(null);
|
|
const [mapZoomBounds, setMapZoomBounds] = useState<{
|
|
min_x: number;
|
|
min_y: number;
|
|
max_x: number;
|
|
max_y: number;
|
|
} | null>(null);
|
|
const [parcelGeometries, setParcelGeometries] = useState<ParcelGeometry[]>([]);
|
|
const [selectionSummary, setSelectionSummary] = useState<{
|
|
combinedOutline?: { type: 'Polygon' | 'MultiPolygon'; coordinates: number[][][] | number[][][][] };
|
|
total_area_m2?: number;
|
|
bauzonen?: Array<{ bauzone: string; parcels: any[]; area_m2: number }>;
|
|
} | null>(null);
|
|
|
|
// Command processing state
|
|
const [commandInput, setCommandInput] = useState<string>('');
|
|
const [isProcessingCommand, setIsProcessingCommand] = useState(false);
|
|
const [commandResults, setCommandResults] = useState<any[]>([]);
|
|
const [commandError, setCommandError] = useState<string | null>(null);
|
|
|
|
// Project state
|
|
const [currentProjekt, setCurrentProjekt] = useState<Projekt | null>(null);
|
|
const [isCreatingProjekt, setIsCreatingProjekt] = useState(false);
|
|
const [isAddingParcel, setIsAddingParcel] = useState(false);
|
|
const [projektError, setProjektError] = useState<string | null>(null);
|
|
|
|
// Panel state
|
|
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
|
|
|
// Derive parcelGeometries ONLY from selected parcels (deselected = not drawn)
|
|
useEffect(() => {
|
|
const geometries: ParcelGeometry[] = selectedParcels.map(p => {
|
|
let coords: MapPoint[] = [];
|
|
const geo = p.map_view?.geometry_geojson?.geometry;
|
|
if (geo?.coordinates?.[0]) {
|
|
coords = geo.coordinates[0].map((c: number[]) => ({ x: c[0], y: c[1] }));
|
|
} else if (p.parcel.perimeter?.punkte) {
|
|
coords = p.parcel.perimeter.punkte.map((pt: { x: number; y: number }) => ({ x: pt.x, y: pt.y }));
|
|
}
|
|
return {
|
|
id: p.parcel.id,
|
|
egrid: p.parcel.egrid,
|
|
number: p.parcel.number,
|
|
coordinates: coords,
|
|
isSelected: true,
|
|
};
|
|
}).filter(g => g.coordinates.length >= 3);
|
|
setParcelGeometries(geometries);
|
|
}, [selectedParcels]);
|
|
|
|
// Fetch selection summary when selected parcels change
|
|
useEffect(() => {
|
|
if (selectedParcels.length === 0) {
|
|
setSelectionSummary(null);
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
api.post('/api/realestate/parcel/selection-summary', { parcels: selectedParcels })
|
|
.then((res) => {
|
|
if (!cancelled) {
|
|
setSelectionSummary({
|
|
combinedOutline: res.data.combined_outline_geojson,
|
|
total_area_m2: res.data.total_area_m2,
|
|
bauzonen: res.data.bauzonen,
|
|
});
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
if (!cancelled && import.meta.env.DEV) {
|
|
console.warn('Selection summary fetch failed:', err);
|
|
}
|
|
});
|
|
return () => { cancelled = true; };
|
|
}, [selectedParcels]);
|
|
|
|
/**
|
|
* Get current geolocation and directly search for parcel
|
|
* Does not fill input fields, directly makes the request
|
|
*/
|
|
const useCurrentLocation = useCallback(async () => {
|
|
setIsGettingLocation(true);
|
|
setLocationError(null);
|
|
|
|
try {
|
|
if (!navigator.geolocation) {
|
|
throw new Error('Geolocation wird von Ihrem Browser nicht unterstützt');
|
|
}
|
|
|
|
return new Promise<void>((resolve, reject) => {
|
|
navigator.geolocation.getCurrentPosition(
|
|
async (position) => {
|
|
try {
|
|
// Convert WGS84 to LV95 using the converter function
|
|
const lat = position.coords.latitude;
|
|
const lon = position.coords.longitude;
|
|
|
|
const lv95 = wgs84ToLV95(lat, lon);
|
|
const locationString = `${Math.round(lv95.x)},${Math.round(lv95.y)}`;
|
|
|
|
// Directly search for parcel without updating input fields
|
|
await searchParcel(locationString);
|
|
resolve();
|
|
} catch (err: any) {
|
|
setLocationError(err.message || 'Fehler beim Konvertieren der Koordinaten');
|
|
reject(err);
|
|
}
|
|
},
|
|
(error) => {
|
|
let errorMessage = 'Fehler beim Abrufen der Position';
|
|
switch (error.code) {
|
|
case error.PERMISSION_DENIED:
|
|
errorMessage = 'Zugriff auf Standort wurde verweigert';
|
|
break;
|
|
case error.POSITION_UNAVAILABLE:
|
|
errorMessage = 'Standortinformationen nicht verfügbar';
|
|
break;
|
|
case error.TIMEOUT:
|
|
errorMessage = 'Zeitüberschreitung beim Abrufen der Position';
|
|
break;
|
|
}
|
|
setLocationError(errorMessage);
|
|
reject(new Error(errorMessage));
|
|
},
|
|
{
|
|
enableHighAccuracy: true,
|
|
timeout: 10000,
|
|
maximumAge: 0
|
|
}
|
|
);
|
|
});
|
|
} catch (err: any) {
|
|
setLocationError(err.message || 'Fehler beim Abrufen der aktuellen Position');
|
|
throw err;
|
|
} finally {
|
|
setIsGettingLocation(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Search for parcel by location (address or coordinates)
|
|
*/
|
|
const searchParcel = useCallback(async (location: string) => {
|
|
if (!location.trim()) {
|
|
setParcelSearchError('Bitte geben Sie einen Standort ein');
|
|
return;
|
|
}
|
|
|
|
setIsSearchingParcel(true);
|
|
setParcelSearchError(null);
|
|
|
|
try {
|
|
const response = await api.get('/api/realestate/parcel/search', {
|
|
params: {
|
|
location: location.trim(),
|
|
include_adjacent: false
|
|
}
|
|
});
|
|
|
|
const data: ParcelSearchResponse = response.data;
|
|
|
|
// Debug logging
|
|
if (import.meta.env.DEV) {
|
|
console.log('📦 Parcel search response:', {
|
|
hasMapView: !!data.map_view,
|
|
hasGeometry: !!data.map_view?.geometry_geojson,
|
|
hasPerimeter: !!data.parcel.perimeter,
|
|
adjacentCount: data.adjacent_parcels?.length || 0
|
|
});
|
|
}
|
|
|
|
// First selection: replace with single parcel
|
|
setSelectedParcels([data]);
|
|
if (data.map_view) {
|
|
setMapCenter(data.map_view.center);
|
|
setMapZoomBounds(data.map_view.zoom_bounds);
|
|
} else if (data.parcel.centroid) {
|
|
setMapCenter(data.parcel.centroid);
|
|
}
|
|
|
|
// Open panel when parcel is found
|
|
setIsPanelOpen(true);
|
|
|
|
return { success: true, data };
|
|
} catch (err: any) {
|
|
const errorMessage =
|
|
err.response?.data?.detail || err.message || 'Fehler beim Suchen der Parzelle';
|
|
setParcelSearchError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setIsSearchingParcel(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Add an adjacent parcel to the selection. Only adjacent parcels can be added.
|
|
*/
|
|
const addAdjacentParcel = useCallback(async (location: { x: number; y: number }) => {
|
|
if (selectedParcels.length === 0) return { success: false, error: 'No selection' };
|
|
setIsSearchingParcel(true);
|
|
setParcelSearchError(null);
|
|
try {
|
|
const res = await api.post('/api/realestate/parcel/add-adjacent', {
|
|
location: { x: location.x, y: location.y },
|
|
selected_parcels: selectedParcels,
|
|
});
|
|
const data: ParcelSearchResponse = res.data;
|
|
const exists = selectedParcels.some(p => p.parcel.id === data.parcel.id);
|
|
if (!exists) {
|
|
setSelectedParcels(prev => [...prev, data]);
|
|
if (data.map_view?.zoom_bounds) {
|
|
setMapZoomBounds(data.map_view.zoom_bounds);
|
|
}
|
|
}
|
|
setIsPanelOpen(true);
|
|
return { success: true, data };
|
|
} catch (err: any) {
|
|
const msg = err.response?.data?.detail || err.message || 'Fehler beim Hinzufügen';
|
|
setParcelSearchError(msg);
|
|
return { success: false, error: msg };
|
|
} finally {
|
|
setIsSearchingParcel(false);
|
|
}
|
|
}, [selectedParcels]);
|
|
|
|
/**
|
|
* Handle map click - search (first selection) or add adjacent (when selection exists)
|
|
*/
|
|
const handleMapClick = useCallback(
|
|
async (point: MapPoint) => {
|
|
const locationString = `${point.x},${point.y}`;
|
|
setLocationInput(locationString);
|
|
if (selectedParcels.length === 0) {
|
|
await searchParcel(locationString);
|
|
} else {
|
|
await addAdjacentParcel({ x: point.x, y: point.y });
|
|
}
|
|
},
|
|
[searchParcel, addAdjacentParcel, selectedParcels.length]
|
|
);
|
|
|
|
/**
|
|
* Check if a parcel is selected
|
|
*/
|
|
const isParcelSelected = useCallback((parcelId: string): boolean => {
|
|
return selectedParcels.some(p => p.parcel.id === parcelId);
|
|
}, [selectedParcels]);
|
|
|
|
/**
|
|
* Remove a parcel from selection. parcelGeometries are derived from selectedParcels.
|
|
*/
|
|
const removeParcel = useCallback((parcelId: string) => {
|
|
setSelectedParcels(prev => prev.filter(p => p.parcel.id !== parcelId));
|
|
}, []);
|
|
|
|
/**
|
|
* Clear all selected parcels
|
|
*/
|
|
const clearSelectedParcels = useCallback(() => {
|
|
setSelectedParcels([]);
|
|
}, []);
|
|
|
|
/**
|
|
* Handle parcel click on map: if panel is hidden, open it; if visible, remove parcel.
|
|
* For combined outline (parcelId === 'combined'), only opens panel, never removes.
|
|
*/
|
|
const handleParcelClick = useCallback((parcelId: string) => {
|
|
if (!isPanelOpen) {
|
|
setIsPanelOpen(true);
|
|
} else if (parcelId !== 'combined') {
|
|
removeParcel(parcelId);
|
|
}
|
|
}, [isPanelOpen, setIsPanelOpen, removeParcel]);
|
|
|
|
/**
|
|
* Process natural language command
|
|
* Always includes the currently selected parcel if available
|
|
*/
|
|
const processCommand = useCallback(async (userInput: string) => {
|
|
if (!userInput.trim()) {
|
|
setCommandError('Bitte geben Sie einen Befehl ein');
|
|
return;
|
|
}
|
|
|
|
setIsProcessingCommand(true);
|
|
setCommandError(null);
|
|
|
|
// Add user message
|
|
const userMessage = {
|
|
id: `user-${Date.now()}`,
|
|
role: 'user',
|
|
message: userInput.trim(),
|
|
timestamp: Date.now()
|
|
};
|
|
setCommandResults((prev) => [...prev, userMessage]);
|
|
|
|
try {
|
|
// Build request body with user input and selected parcel
|
|
const requestBody: any = {
|
|
userInput: userInput.trim()
|
|
};
|
|
|
|
// Always include the currently selected parcels if available
|
|
if (selectedParcels.length > 0) {
|
|
// Use first selected parcel for backward compatibility
|
|
const firstParcel = selectedParcels[0];
|
|
requestBody.selectedParcel = {
|
|
id: firstParcel.parcel.id,
|
|
egrid: firstParcel.parcel.egrid,
|
|
number: firstParcel.parcel.number,
|
|
name: firstParcel.parcel.name,
|
|
identnd: firstParcel.parcel.identnd,
|
|
canton: firstParcel.parcel.canton,
|
|
municipality_code: firstParcel.parcel.municipality_code,
|
|
municipality_name: firstParcel.parcel.municipality_name,
|
|
address: firstParcel.parcel.address,
|
|
area_m2: firstParcel.parcel.area_m2,
|
|
centroid: firstParcel.parcel.centroid,
|
|
geoportal_url: firstParcel.parcel.geoportal_url,
|
|
realestate_type: firstParcel.parcel.realestate_type,
|
|
bauzone: firstParcel.parcel.bauzone,
|
|
zone: firstParcel.parcel.zone,
|
|
// Include geometry data if available
|
|
geometry_geojson: firstParcel.map_view?.geometry_geojson,
|
|
perimeter: firstParcel.parcel.perimeter
|
|
};
|
|
// Also include all selected parcels as array
|
|
requestBody.selectedParcels = selectedParcels.map(p => ({
|
|
id: p.parcel.id,
|
|
egrid: p.parcel.egrid,
|
|
number: p.parcel.number,
|
|
name: p.parcel.name,
|
|
identnd: p.parcel.identnd,
|
|
canton: p.parcel.canton,
|
|
municipality_code: p.parcel.municipality_code,
|
|
municipality_name: p.parcel.municipality_name,
|
|
address: p.parcel.address,
|
|
area_m2: p.parcel.area_m2,
|
|
centroid: p.parcel.centroid,
|
|
geoportal_url: p.parcel.geoportal_url,
|
|
realestate_type: p.parcel.realestate_type,
|
|
bauzone: p.parcel.bauzone,
|
|
zone: p.parcel.zone,
|
|
geometry_geojson: p.map_view?.geometry_geojson,
|
|
perimeter: p.parcel.perimeter
|
|
}));
|
|
}
|
|
|
|
const response = await api.post('/api/realestate/command', requestBody);
|
|
|
|
const data: CommandResponse = response.data;
|
|
|
|
// Format response as assistant message
|
|
let responseMessage = '';
|
|
if (data.success) {
|
|
if (data.result) {
|
|
if (typeof data.result === 'object') {
|
|
responseMessage = `**Intent:** ${data.intent || 'Unknown'}\n**Entity:** ${data.entity || 'N/A'}\n\n**Result:**\n\`\`\`json\n${JSON.stringify(data.result, null, 2)}\n\`\`\``;
|
|
} else {
|
|
responseMessage = `**Intent:** ${data.intent || 'Unknown'}\n**Entity:** ${data.entity || 'N/A'}\n\n**Result:** ${data.result}`;
|
|
}
|
|
} else {
|
|
responseMessage = `Command executed successfully.\n**Intent:** ${data.intent || 'Unknown'}\n**Entity:** ${data.entity || 'N/A'}`;
|
|
}
|
|
} else {
|
|
responseMessage = `Error: ${data.error || 'Unknown error'}`;
|
|
}
|
|
|
|
const assistantMessage = {
|
|
id: `assistant-${Date.now()}`,
|
|
role: 'assistant',
|
|
message: responseMessage,
|
|
timestamp: Date.now()
|
|
};
|
|
setCommandResults((prev) => [...prev, assistantMessage]);
|
|
|
|
// If a project was created and there are selected parcels, automatically add them
|
|
if (data.success && data.intent === 'CREATE' && data.entity === 'Projekt' && selectedParcels.length > 0) {
|
|
try {
|
|
// Extract projekt from result
|
|
const projektResult = data.result?.result || data.result;
|
|
if (projektResult?.id) {
|
|
// Set as current projekt
|
|
setCurrentProjekt(projektResult);
|
|
|
|
// Add all selected parcels to the newly created project via direct API call
|
|
let addedCount = 0;
|
|
for (const selectedParcel of selectedParcels) {
|
|
try {
|
|
const addParcelRequestBody: any = {
|
|
parcelId: selectedParcel.parcel.id,
|
|
parcelData: {
|
|
id: selectedParcel.parcel.id,
|
|
egrid: selectedParcel.parcel.egrid,
|
|
number: selectedParcel.parcel.number,
|
|
name: selectedParcel.parcel.name,
|
|
identnd: selectedParcel.parcel.identnd,
|
|
canton: selectedParcel.parcel.canton,
|
|
municipality_code: selectedParcel.parcel.municipality_code,
|
|
municipality_name: selectedParcel.parcel.municipality_name,
|
|
address: selectedParcel.parcel.address,
|
|
area_m2: selectedParcel.parcel.area_m2,
|
|
centroid: selectedParcel.parcel.centroid,
|
|
geoportal_url: selectedParcel.parcel.geoportal_url,
|
|
realestate_type: selectedParcel.parcel.realestate_type,
|
|
bauzone: selectedParcel.parcel.bauzone,
|
|
zone: selectedParcel.parcel.zone,
|
|
geometry_geojson: selectedParcel.map_view?.geometry_geojson,
|
|
perimeter: selectedParcel.parcel.perimeter
|
|
}
|
|
};
|
|
|
|
const addResponse = await api.post(
|
|
`/api/realestate/projekt/${projektResult.id}/add-parcel`,
|
|
addParcelRequestBody
|
|
);
|
|
const addResult: AddParcelResponse = addResponse.data;
|
|
|
|
// Update current projekt with the updated version that includes the parcel
|
|
setCurrentProjekt(addResult.projekt);
|
|
addedCount++;
|
|
} catch (addError: any) {
|
|
console.error(`Failed to add parcel ${selectedParcel.parcel.id} to project:`, addError);
|
|
}
|
|
}
|
|
|
|
// Update the assistant message to indicate parcels were added
|
|
const parcelText = addedCount === 1 ? 'Parzelle' : 'Parzellen';
|
|
const updateMessage = {
|
|
...assistantMessage,
|
|
id: `assistant-update-${Date.now()}`,
|
|
message: `${responseMessage}\n\n✅ ${addedCount} ${parcelText} ${addedCount === 1 ? 'wurde' : 'wurden'} automatisch zum Projekt hinzugefügt.`
|
|
};
|
|
setCommandResults((prev) => {
|
|
const updated = [...prev];
|
|
const lastIndex = updated.length - 1;
|
|
if (updated[lastIndex]?.id === assistantMessage.id) {
|
|
updated[lastIndex] = updateMessage;
|
|
}
|
|
return updated;
|
|
});
|
|
}
|
|
} catch (addError: any) {
|
|
// Log error but don't fail the command
|
|
console.error('Failed to automatically add parcel to project:', addError);
|
|
const errorMessage = addError.response?.data?.detail || addError.message || 'Unbekannter Fehler';
|
|
const errorUpdate = {
|
|
id: `assistant-error-${Date.now()}`,
|
|
role: 'assistant',
|
|
message: `⚠️ Projekt wurde erstellt, aber Parzelle konnte nicht automatisch hinzugefügt werden: ${errorMessage}`,
|
|
timestamp: Date.now()
|
|
};
|
|
setCommandResults((prev) => [...prev, errorUpdate]);
|
|
}
|
|
}
|
|
|
|
// If a parcel was created and there are selected parcels, automatically populate it with the first selected parcel data
|
|
if (data.success && data.intent === 'CREATE' && data.entity === 'Parzelle' && selectedParcels.length > 0) {
|
|
const selectedParcel = selectedParcels[0]; // Use first selected parcel
|
|
try {
|
|
// Extract parzelle from result
|
|
const parzelleResult = data.result?.result || data.result;
|
|
if (parzelleResult?.id) {
|
|
// Update the newly created parcel with data from the selected parcel
|
|
const updateParcelRequestBody: any = {
|
|
// Map selected parcel data to parzelle fields
|
|
egrid: selectedParcel.parcel.egrid,
|
|
number: selectedParcel.parcel.number,
|
|
name: selectedParcel.parcel.name,
|
|
identnd: selectedParcel.parcel.identnd,
|
|
canton: selectedParcel.parcel.canton,
|
|
municipality_code: selectedParcel.parcel.municipality_code,
|
|
municipality_name: selectedParcel.parcel.municipality_name,
|
|
address: selectedParcel.parcel.address,
|
|
strasseNr: selectedParcel.parcel.address,
|
|
area_m2: selectedParcel.parcel.area_m2,
|
|
centroid: selectedParcel.parcel.centroid,
|
|
geoportal_url: selectedParcel.parcel.geoportal_url,
|
|
realestate_type: selectedParcel.parcel.realestate_type,
|
|
bauzone: selectedParcel.parcel.bauzone,
|
|
zone: selectedParcel.parcel.zone,
|
|
// Include geometry data
|
|
geometry_geojson: selectedParcel.map_view?.geometry_geojson,
|
|
perimeter: selectedParcel.parcel.perimeter
|
|
};
|
|
|
|
// Try to update the parcel via PUT request
|
|
try {
|
|
await api.put(
|
|
`/api/realestate/parzelle/${parzelleResult.id}`,
|
|
updateParcelRequestBody
|
|
);
|
|
|
|
// Update the assistant message to indicate parcel was populated
|
|
const updateMessage = {
|
|
...assistantMessage,
|
|
id: `assistant-update-${Date.now()}`,
|
|
message: `${responseMessage}\n\n✅ Parzelle wurde automatisch mit Daten der Kartenauswahl befüllt.`
|
|
};
|
|
setCommandResults((prev) => {
|
|
const updated = [...prev];
|
|
const lastIndex = updated.length - 1;
|
|
if (updated[lastIndex]?.id === assistantMessage.id) {
|
|
updated[lastIndex] = updateMessage;
|
|
}
|
|
return updated;
|
|
});
|
|
} catch (putError: any) {
|
|
// If PUT doesn't work, try PATCH
|
|
try {
|
|
await api.patch(
|
|
`/api/realestate/parzelle/${parzelleResult.id}`,
|
|
updateParcelRequestBody
|
|
);
|
|
|
|
const updateMessage = {
|
|
...assistantMessage,
|
|
id: `assistant-update-${Date.now()}`,
|
|
message: `${responseMessage}\n\n✅ Parzelle wurde automatisch mit Daten der Kartenauswahl befüllt.`
|
|
};
|
|
setCommandResults((prev) => {
|
|
const updated = [...prev];
|
|
const lastIndex = updated.length - 1;
|
|
if (updated[lastIndex]?.id === assistantMessage.id) {
|
|
updated[lastIndex] = updateMessage;
|
|
}
|
|
return updated;
|
|
});
|
|
} catch (patchError: any) {
|
|
// If both PUT and PATCH fail, log but don't fail the command
|
|
console.error('Failed to update parcel with selected parcel data:', patchError);
|
|
const errorMessage = patchError.response?.data?.detail || patchError.message || 'Unbekannter Fehler';
|
|
const errorUpdate = {
|
|
id: `assistant-error-${Date.now()}`,
|
|
role: 'assistant',
|
|
message: `⚠️ Parzelle wurde erstellt, aber konnte nicht automatisch mit Kartenauswahl-Daten befüllt werden: ${errorMessage}`,
|
|
timestamp: Date.now()
|
|
};
|
|
setCommandResults((prev) => [...prev, errorUpdate]);
|
|
}
|
|
}
|
|
}
|
|
} catch (updateError: any) {
|
|
// Log error but don't fail the command
|
|
console.error('Failed to automatically populate parcel with selected parcel data:', updateError);
|
|
const errorMessage = updateError.response?.data?.detail || updateError.message || 'Unbekannter Fehler';
|
|
const errorUpdate = {
|
|
id: `assistant-error-${Date.now()}`,
|
|
role: 'assistant',
|
|
message: `⚠️ Parzelle wurde erstellt, aber konnte nicht automatisch mit Kartenauswahl-Daten befüllt werden: ${errorMessage}`,
|
|
timestamp: Date.now()
|
|
};
|
|
setCommandResults((prev) => [...prev, errorUpdate]);
|
|
}
|
|
}
|
|
|
|
// Clear input on success
|
|
setCommandInput('');
|
|
|
|
return { success: true, data };
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to send command';
|
|
setCommandError(errorMessage);
|
|
|
|
// Add error message
|
|
const errorMsg = {
|
|
id: `error-${Date.now()}`,
|
|
role: 'assistant',
|
|
message: `**Error:** ${errorMessage}`,
|
|
timestamp: Date.now()
|
|
};
|
|
setCommandResults((prev) => [...prev, errorMsg]);
|
|
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setIsProcessingCommand(false);
|
|
}
|
|
}, [selectedParcels]);
|
|
|
|
/**
|
|
* Create a new project
|
|
*/
|
|
const createProjekt = useCallback(
|
|
async (data: {
|
|
label: string;
|
|
statusProzess?: string;
|
|
location?: string;
|
|
parcelIds?: string[];
|
|
}) => {
|
|
setIsCreatingProjekt(true);
|
|
setProjektError(null);
|
|
|
|
try {
|
|
const requestBody: any = {
|
|
label: data.label
|
|
};
|
|
|
|
if (data.statusProzess) {
|
|
requestBody.statusProzess = data.statusProzess;
|
|
}
|
|
|
|
if (data.location) {
|
|
requestBody.location = data.location;
|
|
}
|
|
|
|
if (data.parcelIds && data.parcelIds.length > 0) {
|
|
requestBody.parcelIds = data.parcelIds;
|
|
}
|
|
|
|
const response = await api.post('/api/realestate/projekt/create', requestBody);
|
|
const result: CreateProjektResponse = response.data;
|
|
|
|
setCurrentProjekt(result.projekt);
|
|
return { success: true, data: result };
|
|
} catch (err: any) {
|
|
const errorMessage =
|
|
err.response?.data?.detail || err.message || 'Fehler beim Erstellen des Projekts';
|
|
setProjektError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setIsCreatingProjekt(false);
|
|
}
|
|
},
|
|
[]
|
|
);
|
|
|
|
/**
|
|
* Add a parcel to an existing project
|
|
*/
|
|
const addParcelToProjekt = useCallback(
|
|
async (
|
|
projektId: string,
|
|
data: {
|
|
parcelId?: string;
|
|
location?: string;
|
|
parcelData?: Record<string, any>;
|
|
}
|
|
) => {
|
|
if (!currentProjekt || currentProjekt.id !== projektId) {
|
|
setProjektError('Projekt nicht gefunden');
|
|
return { success: false, error: 'Projekt nicht gefunden' };
|
|
}
|
|
|
|
setIsAddingParcel(true);
|
|
setProjektError(null);
|
|
|
|
try {
|
|
const requestBody: any = {};
|
|
|
|
if (data.parcelId) {
|
|
requestBody.parcelId = data.parcelId;
|
|
} else if (data.location) {
|
|
requestBody.location = data.location;
|
|
} else if (data.parcelData) {
|
|
requestBody.parcelData = data.parcelData;
|
|
} else {
|
|
throw new Error('Bitte geben Sie parcelId, location oder parcelData an');
|
|
}
|
|
|
|
const response = await api.post(
|
|
`/api/realestate/projekt/${projektId}/add-parcel`,
|
|
requestBody
|
|
);
|
|
const result: AddParcelResponse = response.data;
|
|
|
|
setCurrentProjekt(result.projekt);
|
|
return { success: true, data: result };
|
|
} catch (err: any) {
|
|
const errorMessage =
|
|
err.response?.data?.detail || err.message || 'Fehler beim Hinzufügen der Parzelle';
|
|
setProjektError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setIsAddingParcel(false);
|
|
}
|
|
},
|
|
[currentProjekt]
|
|
);
|
|
|
|
// Build location string from separate fields
|
|
const buildLocationString = useCallback(() => {
|
|
const parts = [kanton, gemeinde, adresse].filter(Boolean);
|
|
return parts.join(', ');
|
|
}, [kanton, gemeinde, adresse]);
|
|
|
|
return {
|
|
instanceId,
|
|
// Location input - separate fields
|
|
kanton,
|
|
setKanton,
|
|
gemeinde,
|
|
setGemeinde,
|
|
adresse,
|
|
setAdresse,
|
|
buildLocationString,
|
|
// Legacy locationInput for backward compatibility
|
|
locationInput,
|
|
setLocationInput,
|
|
useCurrentLocation,
|
|
isGettingLocation,
|
|
locationError,
|
|
|
|
// Parcel search
|
|
selectedParcels,
|
|
searchParcel,
|
|
isSearchingParcel,
|
|
parcelSearchError,
|
|
removeParcel,
|
|
clearSelectedParcels,
|
|
isParcelSelected,
|
|
|
|
// Map view
|
|
mapCenter,
|
|
mapZoomBounds,
|
|
parcelGeometries,
|
|
selectionSummary,
|
|
handleMapClick,
|
|
handleParcelClick,
|
|
|
|
// Command processing
|
|
commandInput,
|
|
setCommandInput,
|
|
processCommand,
|
|
isProcessingCommand,
|
|
commandResults,
|
|
commandError,
|
|
|
|
// Project management
|
|
currentProjekt,
|
|
createProjekt,
|
|
isCreatingProjekt,
|
|
addParcelToProjekt,
|
|
isAddingParcel,
|
|
projektError,
|
|
|
|
// Panel state
|
|
isPanelOpen,
|
|
setIsPanelOpen
|
|
};
|
|
}
|