Merge pull request #2 from valueonag/feat/real-estate

Feat/real estate
This commit is contained in:
idittrich-valueon 2025-12-30 10:00:40 +01:00 committed by GitHub
commit 14273c248a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 617 additions and 128 deletions

132
src/api/attributesApi.ts Normal file
View 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');
}

View file

@ -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';

View file

@ -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,

View file

@ -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}

View file

@ -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;
}
data.adjacent_parcels.forEach((adjacent) => {
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
console.log(`🔍 Fetching geometry for adjacent parcel: ${searchLocation}`); console.log(`🔍 Processing adjacent parcel ${adjacent.id}:`, {
} hasGeometryGeoJson: !!adjacent.geometry_geojson,
hasPerimeter: !!adjacent.perimeter,
const adjResponse = await api.get('/api/realestate/parcel/search', { geometryGeoJson: adjacent.geometry_geojson,
params: { perimeter: adjacent.perimeter
location: searchLocation,
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]
})); }));
if (import.meta.env.DEV) {
console.log(`✅ Extracted ${adjCoordinates.length} coordinates from geometry_geojson for ${adjacent.id}`);
} }
} else if (adjData.parcel.perimeter?.punkte) { }
adjCoordinates = adjData.parcel.perimeter.punkte.map((p) => ({ }
// Fallback to perimeter.punkte if available
else if (adjacent.perimeter?.punkte) {
adjCoordinates = adjacent.perimeter.punkte.map((p) => ({
x: p.x, x: p.x,
y: p.y 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

View 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 '';
}