commit
14273c248a
6 changed files with 617 additions and 128 deletions
132
src/api/attributesApi.ts
Normal file
132
src/api/attributesApi.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { ApiRequestOptions } from '../hooks/useApi';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TYPES & INTERFACES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface AttributeDefinition {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
type: 'string' | 'number' | 'date' | 'boolean' | 'enum' | 'text' | 'email' | 'checkbox' | 'select' | 'multiselect' | 'textarea';
|
||||||
|
sortable?: boolean;
|
||||||
|
filterable?: boolean;
|
||||||
|
searchable?: boolean;
|
||||||
|
width?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
maxWidth?: number;
|
||||||
|
filterOptions?: string[];
|
||||||
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
|
default?: any;
|
||||||
|
options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string;
|
||||||
|
validation?: any;
|
||||||
|
ui?: any;
|
||||||
|
readonly?: boolean;
|
||||||
|
editable?: boolean;
|
||||||
|
visible?: boolean;
|
||||||
|
order?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type for the request function passed to API functions
|
||||||
|
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API REQUEST FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic function to fetch attributes for any entity type
|
||||||
|
* Endpoint: GET /api/attributes/{entityType}
|
||||||
|
*/
|
||||||
|
export async function fetchAttributes(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
entityType: string
|
||||||
|
): Promise<AttributeDefinition[]> {
|
||||||
|
const data = await request<any>({
|
||||||
|
url: `/api/attributes/${entityType}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract attributes from response - check if response.data.attributes exists, otherwise check if response.data is an array
|
||||||
|
let attrs: AttributeDefinition[] = [];
|
||||||
|
if (data?.attributes && Array.isArray(data.attributes)) {
|
||||||
|
attrs = data.attributes;
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
|
attrs = data;
|
||||||
|
} else if (data && typeof data === 'object') {
|
||||||
|
// Try to find any array property in the response
|
||||||
|
const keys = Object.keys(data);
|
||||||
|
for (const key of keys) {
|
||||||
|
if (Array.isArray(data[key])) {
|
||||||
|
attrs = data[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch connection attributes from backend
|
||||||
|
* Endpoint: GET /api/attributes/UserConnection
|
||||||
|
*/
|
||||||
|
export async function fetchConnectionAttributes(request: ApiRequestFunction): Promise<AttributeDefinition[]> {
|
||||||
|
return fetchAttributes(request, 'UserConnection');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch file attributes from backend
|
||||||
|
* Endpoint: GET /api/attributes/FileItem
|
||||||
|
*/
|
||||||
|
export async function fetchFileAttributes(request: ApiRequestFunction): Promise<AttributeDefinition[]> {
|
||||||
|
const data = await request<AttributeDefinition[] | { attributes: AttributeDefinition[] }>({
|
||||||
|
url: '/api/attributes/FileItem',
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle different response formats
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (data && typeof data === 'object' && 'attributes' in data && Array.isArray(data.attributes)) {
|
||||||
|
return data.attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find any array property in the response
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
const keys = Object.keys(data);
|
||||||
|
for (const key of keys) {
|
||||||
|
if (Array.isArray((data as any)[key])) {
|
||||||
|
return (data as any)[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch prompt attributes from backend
|
||||||
|
* Endpoint: GET /api/attributes/Prompt
|
||||||
|
*/
|
||||||
|
export async function fetchPromptAttributes(request: ApiRequestFunction): Promise<AttributeDefinition[]> {
|
||||||
|
return fetchAttributes(request, 'Prompt');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch user attributes from backend
|
||||||
|
* Endpoint: GET /api/attributes/User
|
||||||
|
*/
|
||||||
|
export async function fetchUserAttributes(request: ApiRequestFunction): Promise<AttributeDefinition[]> {
|
||||||
|
return fetchAttributes(request, 'User');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch workflow attributes from backend
|
||||||
|
* Endpoint: GET /api/attributes/ChatWorkflow
|
||||||
|
*/
|
||||||
|
export async function fetchWorkflowAttributes(request: ApiRequestFunction): Promise<AttributeDefinition[]> {
|
||||||
|
return fetchAttributes(request, 'ChatWorkflow');
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,5 @@
|
||||||
// Legacy export - FormGenerator is now FormGeneratorTable (for backward compatibility)
|
export { default as FormGenerator } from './FormGenerator';
|
||||||
export { FormGeneratorTable as FormGenerator } from './FormGeneratorTable';
|
export type { ColumnConfig, FormGeneratorProps } from './FormGenerator';
|
||||||
export type { ColumnConfig, FormGeneratorTableProps as FormGeneratorProps } from './FormGeneratorTable';
|
|
||||||
|
|
||||||
export { FormGeneratorTable } from './FormGeneratorTable';
|
|
||||||
export type { ColumnConfig, FormGeneratorTableProps } from './FormGeneratorTable';
|
|
||||||
|
|
||||||
export { FormGeneratorList } from './FormGeneratorList';
|
|
||||||
export type { FieldConfig, FormGeneratorListProps } from './FormGeneratorList';
|
|
||||||
|
|
||||||
export { FormGeneratorControls } from './FormGeneratorControls';
|
|
||||||
export type { FilterableField, FormGeneratorControlsProps } from './FormGeneratorControls';
|
|
||||||
|
|
||||||
export { FormGeneratorForm } from './FormGeneratorForm';
|
|
||||||
export type { FormGeneratorFormProps, AttributeDefinition, AttributeOption } from './FormGeneratorForm';
|
|
||||||
|
|
||||||
// Re-export action button components and types
|
// Re-export action button components and types
|
||||||
export * from './ActionButtons';
|
export * from './ActionButtons';
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
.fieldsRow {
|
.fieldsRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
align-items: flex-start;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fieldWrapper {
|
.fieldWrapper {
|
||||||
|
|
@ -15,9 +15,8 @@
|
||||||
|
|
||||||
.buttonsWrapper {
|
.buttonsWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 1.5rem;
|
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,9 +34,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonsWrapper {
|
.buttonsWrapper {
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fieldWrapper {
|
.fieldWrapper {
|
||||||
|
|
@ -57,7 +54,6 @@
|
||||||
|
|
||||||
.buttonsWrapper {
|
.buttonsWrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchButton,
|
.searchButton,
|
||||||
|
|
|
||||||
|
|
@ -36,45 +36,6 @@ const PekLocationInput: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.locationInputContainer}>
|
<div className={styles.locationInputContainer}>
|
||||||
<div className={styles.fieldsRow}>
|
<div className={styles.fieldsRow}>
|
||||||
<div className={styles.fieldWrapper}>
|
|
||||||
<TextField
|
|
||||||
value={kanton}
|
|
||||||
onChange={setKanton}
|
|
||||||
placeholder="z.B. BE"
|
|
||||||
label="Kanton"
|
|
||||||
error={locationError && !gemeinde && !adresse ? locationError : undefined}
|
|
||||||
disabled={isGettingLocation || isSearchingParcel}
|
|
||||||
size="md"
|
|
||||||
type="text"
|
|
||||||
name="kanton"
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
const gemeindeInput = document.querySelector('input[name="gemeinde"]') as HTMLInputElement;
|
|
||||||
if (gemeindeInput) gemeindeInput.focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.fieldWrapper}>
|
|
||||||
<TextField
|
|
||||||
value={gemeinde}
|
|
||||||
onChange={setGemeinde}
|
|
||||||
placeholder="z.B. Bern"
|
|
||||||
label="Gemeinde"
|
|
||||||
disabled={isGettingLocation || isSearchingParcel}
|
|
||||||
size="md"
|
|
||||||
type="text"
|
|
||||||
name="gemeinde"
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
const adresseInput = document.querySelector('input[name="adresse"]') as HTMLInputElement;
|
|
||||||
if (adresseInput) adresseInput.focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.fieldWrapper}>
|
<div className={styles.fieldWrapper}>
|
||||||
<TextField
|
<TextField
|
||||||
value={adresse}
|
value={adresse}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,27 @@ export interface ParcelSearchResponse {
|
||||||
id: string;
|
id: string;
|
||||||
egrid?: string;
|
egrid?: string;
|
||||||
number?: 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -285,83 +306,76 @@ export function usePek() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjacent parcels (if available)
|
// Adjacent parcels (if available)
|
||||||
// Fetch geometries for adjacent parcels
|
// Use geometries from the response (no need to fetch separately)
|
||||||
if (data.adjacent_parcels && includeAdjacent && data.adjacent_parcels.length > 0) {
|
if (data.adjacent_parcels && includeAdjacent && data.adjacent_parcels.length > 0) {
|
||||||
// Fetch geometries for each adjacent parcel
|
const adjacentGeometries: ParcelGeometry[] = [];
|
||||||
const adjacentPromises = data.adjacent_parcels.map(async (adjacent) => {
|
|
||||||
try {
|
|
||||||
// Search for the adjacent parcel by its ID or EGRID
|
|
||||||
const searchLocation = adjacent.egrid || adjacent.id || adjacent.number;
|
|
||||||
if (!searchLocation) {
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
console.warn(`⚠️ Adjacent parcel ${adjacent.id} has no search location`);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
data.adjacent_parcels.forEach((adjacent) => {
|
||||||
console.log(`🔍 Fetching geometry for adjacent parcel: ${searchLocation}`);
|
if (import.meta.env.DEV) {
|
||||||
}
|
console.log(`🔍 Processing adjacent parcel ${adjacent.id}:`, {
|
||||||
|
hasGeometryGeoJson: !!adjacent.geometry_geojson,
|
||||||
const adjResponse = await api.get('/api/realestate/parcel/search', {
|
hasPerimeter: !!adjacent.perimeter,
|
||||||
params: {
|
geometryGeoJson: adjacent.geometry_geojson,
|
||||||
location: searchLocation,
|
perimeter: adjacent.perimeter
|
||||||
include_adjacent: false // Don't fetch adjacent of adjacent
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const adjData: ParcelSearchResponse = adjResponse.data;
|
let adjCoordinates: MapPoint[] = [];
|
||||||
let adjCoordinates: MapPoint[] = [];
|
|
||||||
|
|
||||||
// Extract coordinates from adjacent parcel
|
// Extract coordinates from geometry_geojson if available
|
||||||
if (adjData.map_view?.geometry_geojson?.geometry?.coordinates) {
|
if (adjacent.geometry_geojson?.geometry?.coordinates) {
|
||||||
const coords = adjData.map_view.geometry_geojson.geometry.coordinates[0];
|
const coords = adjacent.geometry_geojson.geometry.coordinates[0];
|
||||||
if (Array.isArray(coords)) {
|
if (Array.isArray(coords) && coords.length > 0) {
|
||||||
adjCoordinates = coords.map((coord: number[]) => ({
|
adjCoordinates = coords.map((coord: number[]) => ({
|
||||||
x: coord[0],
|
x: coord[0],
|
||||||
y: coord[1]
|
y: coord[1]
|
||||||
}));
|
|
||||||
}
|
|
||||||
} else if (adjData.parcel.perimeter?.punkte) {
|
|
||||||
adjCoordinates = adjData.parcel.perimeter.punkte.map((p) => ({
|
|
||||||
x: p.x,
|
|
||||||
y: p.y
|
|
||||||
}));
|
}));
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log(`✅ Extracted ${adjCoordinates.length} coordinates from geometry_geojson for ${adjacent.id}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Fallback to perimeter.punkte if available
|
||||||
|
else if (adjacent.perimeter?.punkte) {
|
||||||
|
adjCoordinates = adjacent.perimeter.punkte.map((p) => ({
|
||||||
|
x: p.x,
|
||||||
|
y: p.y
|
||||||
|
}));
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
console.log(`✅ Fetched ${adjCoordinates.length} coordinates for adjacent parcel ${adjacent.id}`);
|
console.log(`✅ Extracted ${adjCoordinates.length} coordinates from perimeter for ${adjacent.id}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
// Only add if we have valid coordinates
|
||||||
|
if (adjCoordinates.length >= 3) {
|
||||||
|
adjacentGeometries.push({
|
||||||
id: adjacent.id,
|
id: adjacent.id,
|
||||||
egrid: adjacent.egrid,
|
egrid: adjacent.egrid,
|
||||||
number: adjacent.number,
|
number: adjacent.number,
|
||||||
coordinates: adjCoordinates,
|
coordinates: adjCoordinates,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isAdjacent: true
|
isAdjacent: true
|
||||||
};
|
});
|
||||||
} catch (err) {
|
} else if (import.meta.env.DEV) {
|
||||||
// If fetching fails, log error but don't add parcel
|
console.warn(`⚠️ Adjacent parcel ${adjacent.id} has insufficient geometry data:`, {
|
||||||
if (import.meta.env.DEV) {
|
coordCount: adjCoordinates.length,
|
||||||
console.error(`❌ Failed to fetch geometry for adjacent parcel ${adjacent.id}:`, err);
|
hasGeometryGeoJson: !!adjacent.geometry_geojson,
|
||||||
}
|
hasPerimeter: !!adjacent.perimeter,
|
||||||
return null;
|
geometryGeoJsonStructure: adjacent.geometry_geojson ? {
|
||||||
|
hasGeometry: !!adjacent.geometry_geojson.geometry,
|
||||||
|
hasCoordinates: !!adjacent.geometry_geojson.geometry?.coordinates,
|
||||||
|
coordinatesLength: adjacent.geometry_geojson.geometry?.coordinates?.length,
|
||||||
|
firstCoordLength: adjacent.geometry_geojson.geometry?.coordinates?.[0]?.length
|
||||||
|
} : null
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for all adjacent parcel geometries
|
|
||||||
const adjacentGeometries = await Promise.all(adjacentPromises);
|
|
||||||
const validAdjacentGeometries = adjacentGeometries.filter(
|
|
||||||
(g): g is ParcelGeometry => g !== null && g.coordinates.length >= 3
|
|
||||||
);
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
console.log(`📦 Adjacent parcels summary:`, {
|
console.log(`📦 Adjacent parcels summary:`, {
|
||||||
requested: data.adjacent_parcels.length,
|
requested: data.adjacent_parcels.length,
|
||||||
fetched: adjacentGeometries.filter(g => g !== null).length,
|
valid: adjacentGeometries.length,
|
||||||
valid: validAdjacentGeometries.length,
|
geometries: adjacentGeometries.map(g => ({
|
||||||
geometries: validAdjacentGeometries.map(g => ({
|
|
||||||
id: g.id,
|
id: g.id,
|
||||||
number: g.number,
|
number: g.number,
|
||||||
coordCount: g.coordinates.length
|
coordCount: g.coordinates.length
|
||||||
|
|
@ -370,7 +384,7 @@ export function usePek() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add adjacent parcels to geometries array
|
// Add adjacent parcels to geometries array
|
||||||
geometries.push(...validAdjacentGeometries);
|
geometries.push(...adjacentGeometries);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update parcel geometries with all parcels (main + adjacent)
|
// Update parcel geometries with all parcels (main + adjacent)
|
||||||
|
|
@ -430,20 +444,47 @@ export function usePek() {
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle parcel click on map
|
* Handle parcel click on map - select the clicked parcel
|
||||||
*/
|
*/
|
||||||
const handleParcelClick = useCallback(async (parcelId: string) => {
|
const handleParcelClick = useCallback(async (parcelId: string) => {
|
||||||
// Re-search for this specific parcel with adjacent parcels
|
// Find the clicked parcel in the geometries
|
||||||
if (selectedParcel) {
|
const clickedParcel = parcelGeometries.find(p => p.id === parcelId);
|
||||||
const locationString = selectedParcel.parcel.centroid
|
|
||||||
? `${selectedParcel.parcel.centroid.x},${selectedParcel.parcel.centroid.y}`
|
if (clickedParcel && clickedParcel.coordinates.length > 0) {
|
||||||
: locationInput;
|
// Use a point inside the parcel (first coordinate is always on the boundary, which is inside)
|
||||||
await searchParcel(locationString, true);
|
// For better accuracy, use a point slightly inside the boundary
|
||||||
|
const firstCoord = clickedParcel.coordinates[0];
|
||||||
|
|
||||||
|
// Calculate centroid as fallback, but prefer a point we know is inside
|
||||||
|
const sumX = clickedParcel.coordinates.reduce((sum, coord) => sum + coord.x, 0);
|
||||||
|
const sumY = clickedParcel.coordinates.reduce((sum, coord) => sum + coord.y, 0);
|
||||||
|
const centroidX = sumX / clickedParcel.coordinates.length;
|
||||||
|
const centroidY = sumY / clickedParcel.coordinates.length;
|
||||||
|
|
||||||
|
// Use first coordinate (guaranteed to be on/in the parcel) for search
|
||||||
|
const locationString = `${firstCoord.x},${firstCoord.y}`;
|
||||||
|
await searchParcel(locationString, true); // Always include adjacent parcels
|
||||||
|
} else {
|
||||||
|
// Fallback: try to search by parcel ID/EGRID if available
|
||||||
|
if (selectedParcel?.adjacent_parcels) {
|
||||||
|
const adjacentParcel = selectedParcel.adjacent_parcels.find(p => p.id === parcelId);
|
||||||
|
if (adjacentParcel?.egrid) {
|
||||||
|
// Search by EGRID
|
||||||
|
await searchParcel(adjacentParcel.egrid, true);
|
||||||
|
} else if (adjacentParcel?.number) {
|
||||||
|
// Try searching by number (might need address context)
|
||||||
|
await searchParcel(adjacentParcel.number, true);
|
||||||
|
} else if (adjacentParcel?.id) {
|
||||||
|
// Last resort: try searching by ID
|
||||||
|
await searchParcel(adjacentParcel.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [selectedParcel, locationInput, searchParcel]);
|
}, [parcelGeometries, selectedParcel, searchParcel]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process natural language command
|
* Process natural language command
|
||||||
|
* Always includes the currently selected parcel if available
|
||||||
*/
|
*/
|
||||||
const processCommand = useCallback(async (userInput: string) => {
|
const processCommand = useCallback(async (userInput: string) => {
|
||||||
if (!userInput.trim()) {
|
if (!userInput.trim()) {
|
||||||
|
|
@ -464,9 +505,34 @@ export function usePek() {
|
||||||
setCommandResults((prev) => [...prev, userMessage]);
|
setCommandResults((prev) => [...prev, userMessage]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.post('/api/realestate/command', {
|
// Build request body with user input and selected parcel
|
||||||
|
const requestBody: any = {
|
||||||
userInput: userInput.trim()
|
userInput: userInput.trim()
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Always include the currently selected parcel if available
|
||||||
|
if (selectedParcel) {
|
||||||
|
requestBody.selectedParcel = {
|
||||||
|
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,
|
||||||
|
// Include geometry data if available
|
||||||
|
geometry_geojson: selectedParcel.map_view?.geometry_geojson,
|
||||||
|
perimeter: selectedParcel.parcel.perimeter
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post('/api/realestate/command', requestBody);
|
||||||
|
|
||||||
const data: CommandResponse = response.data;
|
const data: CommandResponse = response.data;
|
||||||
|
|
||||||
|
|
@ -494,6 +560,172 @@ export function usePek() {
|
||||||
};
|
};
|
||||||
setCommandResults((prev) => [...prev, assistantMessage]);
|
setCommandResults((prev) => [...prev, assistantMessage]);
|
||||||
|
|
||||||
|
// If a project was created and there's a selected parcel, automatically add it
|
||||||
|
if (data.success && data.intent === 'CREATE' && data.entity === 'Projekt' && selectedParcel) {
|
||||||
|
try {
|
||||||
|
// Extract projekt from result
|
||||||
|
const projektResult = data.result?.result || data.result;
|
||||||
|
if (projektResult?.id) {
|
||||||
|
// Set as current projekt
|
||||||
|
setCurrentProjekt(projektResult);
|
||||||
|
|
||||||
|
// Add the selected parcel to the newly created project via direct API call
|
||||||
|
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,
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Update the assistant message to indicate parcel was added
|
||||||
|
const updateMessage = {
|
||||||
|
...assistantMessage,
|
||||||
|
id: `assistant-update-${Date.now()}`,
|
||||||
|
message: `${responseMessage}\n\n✅ Parzelle wurde 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's a selected parcel, automatically populate it with the selected parcel data
|
||||||
|
if (data.success && data.intent === 'CREATE' && data.entity === 'Parzelle' && selectedParcel) {
|
||||||
|
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,
|
||||||
|
// Include geometry data
|
||||||
|
geometry_geojson: selectedParcel.map_view?.geometry_geojson,
|
||||||
|
perimeter: selectedParcel.parcel.perimeter
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to update the parcel via PUT request
|
||||||
|
try {
|
||||||
|
const updateResponse = 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
|
// Clear input on success
|
||||||
setCommandInput('');
|
setCommandInput('');
|
||||||
|
|
||||||
|
|
@ -515,7 +747,7 @@ export function usePek() {
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessingCommand(false);
|
setIsProcessingCommand(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [selectedParcel]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new project
|
* Create a new project
|
||||||
|
|
|
||||||
181
src/utils/attributeTypeMapper.ts
Normal file
181
src/utils/attributeTypeMapper.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
/**
|
||||||
|
* Utility functions for mapping attribute types to HTML input types and component types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type AttributeType =
|
||||||
|
| 'text'
|
||||||
|
| 'textarea'
|
||||||
|
| 'select'
|
||||||
|
| 'multiselect'
|
||||||
|
| 'integer'
|
||||||
|
| 'float'
|
||||||
|
| 'number'
|
||||||
|
| 'timestamp'
|
||||||
|
| 'date'
|
||||||
|
| 'time'
|
||||||
|
| 'checkbox'
|
||||||
|
| 'boolean'
|
||||||
|
| 'email'
|
||||||
|
| 'url'
|
||||||
|
| 'password'
|
||||||
|
| 'file'
|
||||||
|
| 'string'
|
||||||
|
| 'enum'
|
||||||
|
| 'readonly';
|
||||||
|
|
||||||
|
export type InputComponentType =
|
||||||
|
| 'text'
|
||||||
|
| 'textarea'
|
||||||
|
| 'select'
|
||||||
|
| 'multiselect'
|
||||||
|
| 'checkbox'
|
||||||
|
| 'file'
|
||||||
|
| 'email'
|
||||||
|
| 'url'
|
||||||
|
| 'password'
|
||||||
|
| 'date'
|
||||||
|
| 'time'
|
||||||
|
| 'datetime-local'
|
||||||
|
| 'number';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps attribute type to HTML input type
|
||||||
|
*
|
||||||
|
* @param attributeType - The attribute type from the backend
|
||||||
|
* @returns The corresponding HTML input type
|
||||||
|
*
|
||||||
|
* Mapping rules:
|
||||||
|
* - text → text (single line)
|
||||||
|
* - textarea → textarea (multi-line)
|
||||||
|
* - select → select (dropdown with options)
|
||||||
|
* - multiselect → multiselect (multiple selection)
|
||||||
|
* - integer → number (integer only)
|
||||||
|
* - float or number → number (decimal allowed)
|
||||||
|
* - timestamp → datetime-local (date/time picker)
|
||||||
|
* - date → date (date picker, date only)
|
||||||
|
* - time → time (time picker, time only)
|
||||||
|
* - checkbox or boolean → checkbox (boolean)
|
||||||
|
* - email → email (with email validation)
|
||||||
|
* - url → url (with URL validation)
|
||||||
|
* - password → password (masked)
|
||||||
|
* - file → file (file upload)
|
||||||
|
*/
|
||||||
|
export function attributeTypeToInputType(attributeType: AttributeType): InputComponentType {
|
||||||
|
switch (attributeType) {
|
||||||
|
case 'text':
|
||||||
|
case 'string':
|
||||||
|
return 'text';
|
||||||
|
|
||||||
|
case 'textarea':
|
||||||
|
return 'textarea';
|
||||||
|
|
||||||
|
case 'select':
|
||||||
|
case 'enum':
|
||||||
|
return 'select';
|
||||||
|
|
||||||
|
case 'multiselect':
|
||||||
|
return 'multiselect';
|
||||||
|
|
||||||
|
case 'integer':
|
||||||
|
case 'number':
|
||||||
|
case 'float':
|
||||||
|
return 'number';
|
||||||
|
|
||||||
|
case 'timestamp':
|
||||||
|
return 'datetime-local';
|
||||||
|
|
||||||
|
case 'date':
|
||||||
|
return 'date';
|
||||||
|
|
||||||
|
case 'time':
|
||||||
|
return 'time';
|
||||||
|
|
||||||
|
case 'checkbox':
|
||||||
|
case 'boolean':
|
||||||
|
return 'checkbox';
|
||||||
|
|
||||||
|
case 'email':
|
||||||
|
return 'email';
|
||||||
|
|
||||||
|
case 'url':
|
||||||
|
return 'url';
|
||||||
|
|
||||||
|
case 'password':
|
||||||
|
return 'password';
|
||||||
|
|
||||||
|
case 'file':
|
||||||
|
return 'file';
|
||||||
|
|
||||||
|
case 'readonly':
|
||||||
|
return 'text'; // Default to text for readonly, but should be rendered as readonly
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Default fallback to text input
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an attribute type should render as a textarea
|
||||||
|
*/
|
||||||
|
export function isTextareaType(attributeType: AttributeType): boolean {
|
||||||
|
return attributeType === 'textarea';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an attribute type should render as a select dropdown
|
||||||
|
*/
|
||||||
|
export function isSelectType(attributeType: AttributeType): boolean {
|
||||||
|
return attributeType === 'select' || attributeType === 'enum';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an attribute type should render as a multiselect
|
||||||
|
*/
|
||||||
|
export function isMultiselectType(attributeType: AttributeType): boolean {
|
||||||
|
return attributeType === 'multiselect';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an attribute type should render as a checkbox
|
||||||
|
*/
|
||||||
|
export function isCheckboxType(attributeType: AttributeType): boolean {
|
||||||
|
return attributeType === 'checkbox' || attributeType === 'boolean';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an attribute type should render as a file input
|
||||||
|
*/
|
||||||
|
export function isFileType(attributeType: AttributeType): boolean {
|
||||||
|
return attributeType === 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an attribute type should render as a number input
|
||||||
|
*/
|
||||||
|
export function isNumberType(attributeType: AttributeType): boolean {
|
||||||
|
return attributeType === 'integer' || attributeType === 'number' || attributeType === 'float';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an attribute type should render as a date/time input
|
||||||
|
*/
|
||||||
|
export function isDateTimeType(attributeType: AttributeType): boolean {
|
||||||
|
return attributeType === 'timestamp' || attributeType === 'date' || attributeType === 'time';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the default value for an attribute type
|
||||||
|
*/
|
||||||
|
export function getDefaultValueForType(attributeType: AttributeType): any {
|
||||||
|
if (isCheckboxType(attributeType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isMultiselectType(attributeType)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (isNumberType(attributeType)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue