From bfbe3f88693d92a4419e0d8fcbdb532fceb944e3 Mon Sep 17 00:00:00 2001 From: Ida Dittrich Date: Mon, 22 Dec 2025 07:31:56 +0100 Subject: [PATCH] PEK updates --- src/api/attributesApi.ts | 132 ++++++++++++ .../pages/pek/PekLocationInput.module.css | 8 +- .../data/pages/pek/PekLocationInput.tsx | 39 ---- src/hooks/usePek.ts | 198 +++++++++++++++++- src/utils/attributeTypeMapper.ts | 181 ++++++++++++++++ 5 files changed, 510 insertions(+), 48 deletions(-) create mode 100644 src/api/attributesApi.ts create mode 100644 src/utils/attributeTypeMapper.ts diff --git a/src/api/attributesApi.ts b/src/api/attributesApi.ts new file mode 100644 index 0000000..28f9600 --- /dev/null +++ b/src/api/attributesApi.ts @@ -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) => Promise; + +// ============================================================================ +// 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 { + const data = await request({ + 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 { + return fetchAttributes(request, 'UserConnection'); +} + +/** + * Fetch file attributes from backend + * Endpoint: GET /api/attributes/FileItem + */ +export async function fetchFileAttributes(request: ApiRequestFunction): Promise { + const data = await request({ + 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 { + return fetchAttributes(request, 'Prompt'); +} + +/** + * Fetch user attributes from backend + * Endpoint: GET /api/attributes/User + */ +export async function fetchUserAttributes(request: ApiRequestFunction): Promise { + return fetchAttributes(request, 'User'); +} + +/** + * Fetch workflow attributes from backend + * Endpoint: GET /api/attributes/ChatWorkflow + */ +export async function fetchWorkflowAttributes(request: ApiRequestFunction): Promise { + return fetchAttributes(request, 'ChatWorkflow'); +} diff --git a/src/core/PageManager/data/pages/pek/PekLocationInput.module.css b/src/core/PageManager/data/pages/pek/PekLocationInput.module.css index afed4e3..463346b 100644 --- a/src/core/PageManager/data/pages/pek/PekLocationInput.module.css +++ b/src/core/PageManager/data/pages/pek/PekLocationInput.module.css @@ -6,7 +6,7 @@ .fieldsRow { display: flex; gap: 1rem; - align-items: flex-start; + align-items: flex-end; } .fieldWrapper { @@ -15,9 +15,8 @@ .buttonsWrapper { display: flex; - flex-direction: column; + flex-direction: row; gap: 0.5rem; - margin-top: 1.5rem; min-width: 150px; } @@ -35,9 +34,7 @@ } .buttonsWrapper { - flex-direction: row; width: 100%; - margin-top: 0.5rem; } .fieldWrapper { @@ -57,7 +54,6 @@ .buttonsWrapper { width: 100%; - margin-top: 0.5rem; } .searchButton, diff --git a/src/core/PageManager/data/pages/pek/PekLocationInput.tsx b/src/core/PageManager/data/pages/pek/PekLocationInput.tsx index 71b8480..6f73875 100644 --- a/src/core/PageManager/data/pages/pek/PekLocationInput.tsx +++ b/src/core/PageManager/data/pages/pek/PekLocationInput.tsx @@ -36,45 +36,6 @@ const PekLocationInput: React.FC = () => { return (
-
- { - if (e.key === 'Enter') { - e.preventDefault(); - const gemeindeInput = document.querySelector('input[name="gemeinde"]') as HTMLInputElement; - if (gemeindeInput) gemeindeInput.focus(); - } - }} - /> -
-
- { - if (e.key === 'Enter') { - e.preventDefault(); - const adresseInput = document.querySelector('input[name="adresse"]') as HTMLInputElement; - if (adresseInput) adresseInput.focus(); - } - }} - /> -
{ if (!userInput.trim()) { @@ -504,9 +505,34 @@ export function usePek() { setCommandResults((prev) => [...prev, userMessage]); try { - const response = await api.post('/api/realestate/command', { + // Build request body with user input and selected parcel + const requestBody: any = { 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; @@ -534,6 +560,172 @@ export function usePek() { }; 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 setCommandInput(''); @@ -555,7 +747,7 @@ export function usePek() { } finally { setIsProcessingCommand(false); } - }, []); + }, [selectedParcel]); /** * Create a new project diff --git a/src/utils/attributeTypeMapper.ts b/src/utils/attributeTypeMapper.ts new file mode 100644 index 0000000..b0c02b7 --- /dev/null +++ b/src/utils/attributeTypeMapper.ts @@ -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 ''; +}