diff --git a/src/App.tsx b/src/App.tsx index 6fad20c..c8269b0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { CssBaseline, ThemeProvider, createTheme, Box } from '@mui/material'; import { v4 as uuidv4 } from 'uuid'; import { tokens } from './theme/tokens'; -import { generateUUID, transformFlowForExport } from './utils/uuidUtils'; +import { transformFlowForExport } from './utils/uuidUtils'; import styled from 'styled-components'; import FlowMetadataDialog, { FlowMetadata } from './components/FlowMetadataDialog/FlowMetadataDialog'; import ModuleManagerDialog from './components/ModuleManager/ModuleManagerDialog'; @@ -31,24 +31,7 @@ import { SchemaProvider } from './context/SchemaContext'; import { SubflowProvider } from './context/SubflowContext'; import { UserPreferencesProvider } from './context/UserPreferencesContext'; import { ListingFlow, PatternLibraryElement } from './models/listingFlow'; -import { - TextUIElement, - BooleanUIElement, - SingleSelectionUIElement, - NumberUIElement, - DateUIElement, - FileUIElement, - GroupUIElement, - ArrayUIElement, - CustomUIElement, - ChipGroupUIElement, - StringUIElement, - KeyValueListUIElement, - ImageGalleryUIElement, - FieldTextUIElement, - TableUIElement, - ContactUIElement -} from './models/uiElements'; +import { createElement } from './utils/elementFactory'; const theme = createTheme({ palette: { @@ -177,164 +160,6 @@ const MainContent = styled.div` // border-left: 1px solid #ddd; // `; -/** - * Erzeugt ein KeyValueListUIElement für Massedaten (Bill of quantities) - * Diese Funktion wird verwendet, um automatisch das KeyValueListUIElement in ROOM und ROOM_GROUP Subflows einzufügen - * @returns Ein KeyValueListUIElement mit vordefinierten Massedaten-Feldern - */ -const createBillOfQuantitiesElement = (): KeyValueListUIElement => { - return { - pattern_type: 'KeyValueListUIElement', - type: 'TABLE', - required: false, - items: [ - { - icon: 'mdiFloorPlan', - key: { - en: 'Nettogrundfläche (NGF)', - de: 'Nettogrundfläche (NGF)' - }, - field_value: { - field_id: { - field_name: `netto_raumflaeche_${uuidv4().substring(0, 8)}` - } - } - }, - { - icon: 'mdiCube', - key: { - en: 'Netto-Rauminhalt (NRI)', - de: 'Netto-Rauminhalt (NRI)' - }, - field_value: { - field_id: { - field_name: `netto_rauminhalt_${uuidv4().substring(0, 8)}` - } - } - }, - { - icon: 'mdiFloorPlan', - key: { - en: 'Bruttogrundfläche (BGF)', - de: 'Bruttogrundfläche (BGF)' - }, - field_value: { - field_id: { - field_name: `brutto_grundflaeche_${uuidv4().substring(0, 8)}` - } - } - }, - { - icon: 'mdiCube', - key: { - en: 'Brutto-Rauminhalt (BRI)', - de: 'Brutto-Rauminhalt (BRI)' - }, - field_value: { - field_id: { - field_name: `brutto_rauminhalt_${uuidv4().substring(0, 8)}` - } - } - }, - { - icon: 'mdiHeatingCoil', - key: { - en: 'Technikfläche (TF)', - de: 'Technikfläche (TF)' - }, - field_value: { - field_id: { - field_name: `technik_flaeche_${uuidv4().substring(0, 8)}` - } - } - }, - { - icon: 'mdiWalk', - key: { - en: 'Verkehrsfläche (VF)', - de: 'Verkehrsfläche (VF)' - }, - field_value: { - field_id: { - field_name: `verkehrs_flaeche_${uuidv4().substring(0, 8)}` - } - } - }, - { - icon: 'mdiHumanMaleHeight', - key: { - en: 'Room height', - de: 'Raumhöhe' - }, - field_value: { - field_id: { - field_name: `room_height_${uuidv4().substring(0, 8)}` - } - } - }, - { - icon: 'mdiFloorPlan', - key: { - en: 'Room perimeter', - de: 'Raumumfang' - }, - field_value: { - field_id: { - field_name: `slab_perimeter_${uuidv4().substring(0, 8)}` - } - } - }, - { - icon: 'mdiWall', - key: { - en: 'Inside area (net)', - de: 'Innenfläche (netto)' - }, - field_value: { - field_id: { - field_name: `inside_area_net_${uuidv4().substring(0, 8)}` - } - } - }, - { - icon: 'mdiWall', - key: { - en: 'Inside area (gross)', - de: 'Innenfläche (brutto)' - }, - field_value: { - field_id: { - field_name: `inside_area_gross_${uuidv4().substring(0, 8)}` - } - } - }, - { - icon: 'mdiWall', - key: { - en: 'Outside area (net)', - de: 'Außenfläche (netto)' - }, - field_value: { - field_id: { - field_name: `outside_area_net_${uuidv4().substring(0, 8)}` - } - } - }, - { - icon: 'mdiWall', - key: { - en: 'Outside area (gross)', - de: 'Außenfläche (brutto)' - }, - field_value: { - field_id: { - field_name: `outside_area_gross_${uuidv4().substring(0, 8)}` - } - } - } - ] - }; -}; // Beispiel für eine leere ListingFlow-Struktur const emptyFlow: ListingFlow = { @@ -366,1274 +191,7 @@ const emptyFlow: ListingFlow = { }; // Funktion zum Erstellen eines Elements basierend auf dem angegebenen Typ -const createElement = (type: string): PatternLibraryElement => { - switch (type) { - case 'TextUIElement': - return { - element: { - pattern_type: 'TextUIElement', - required: false, - type: 'PARAGRAPH', - text: { - de: 'Beispieltext', - en: 'Example text' - }, - title: { - de: 'Text Element', - en: 'Text Element' - } - } as TextUIElement - }; - - case 'BooleanUIElement': - return { - element: { - pattern_type: 'BooleanUIElement', - required: false, - field_id: { - field_name: `boolean_field_${uuidv4()}` - }, - default_value: false, - title: { - de: 'Boolean Element', - en: 'Boolean Element' - } - } as BooleanUIElement - }; - - case 'SingleSelectionUIElement': - return { - element: { - pattern_type: 'SingleSelectionUIElement', - required: false, - field_id: { - field_name: `selection_field_${uuidv4()}` - }, - type: 'DROPDOWN', - options: [ - { key: uuidv4(), label: { de: 'Option 1', en: 'Option 1' } }, - { key: uuidv4(), label: { de: 'Option 2', en: 'Option 2' } }, - { key: uuidv4(), label: { de: 'Option 3', en: 'Option 3' } } - ], - title: { - de: 'Auswahl Element', - en: 'Selection Element' - } - } as SingleSelectionUIElement - }; - - case 'NumberUIElement': - return { - element: { - pattern_type: 'NumberUIElement', - required: false, - field_id: { - field_name: `number_field_${uuidv4()}` - }, - type: 'INTEGER', - minimum: 0, - maximum: 100, - title: { - de: 'Nummer Element', - en: 'Number Element' - } - } as NumberUIElement - }; - - case 'DateUIElement': - return { - element: { - pattern_type: 'DateUIElement', - required: false, - field_id: { - field_name: `date_field_${uuidv4()}` - }, - // Standardmäßig Jahresauswahl ('Y') entsprechend enion_esg.json - type: 'Y', - title: { - de: 'Datum Element', - en: 'Date Element' - } - } as DateUIElement - }; - - case 'FileUIElement': - const fileUuid = uuidv4(); - return { - element: { - pattern_type: 'FileUIElement', - required: false, - file_type: 'IMAGE', - allowed_file_types: ['image/jpeg', 'image/png'], - // KRITISCH: field_id für eindeutige Identifikation des Elements - field_id: { - field_name: `fileuielement_${fileUuid}` - }, - // KRITISCH: id_field_id für Speicherort der Datei-IDs (eindeutig mit UUID) - id_field_id: { - field_name: `file_images_id_${uuidv4()}` - }, - // EMPFOHLEN: caption_field_id für Bildunterschriften (eindeutig mit UUID) - caption_field_id: { - field_name: `file_images_caption_${uuidv4()}` - }, - // EMPFOHLEN: min_count und max_count definieren - min_count: 0, - max_count: 10, - title: { - de: 'Datei Element', - en: 'File Element' - }, - description: { - de: '', - en: '' - } - } as FileUIElement - }; - - case 'GroupUIElement': - return { - element: { - pattern_type: 'GroupUIElement', - required: false, - isCollapsible: false, - elements: [], // Leere Elemente-Liste - title: { - de: 'Gruppen Element', - en: 'Group Element' - } - } as GroupUIElement - }; - - case 'ArrayUIElement': - return { - element: { - pattern_type: 'ArrayUIElement', - required: false, - field_id: { - field_name: `array_field_${uuidv4()}` - }, - min_count: 0, // Korrigiert: min_count statt min_items - max_count: 10, // Korrigiert: max_count statt max_items - elements: [{ // Korrigiert: elements statt item_template - element: { - pattern_type: 'TextUIElement', - required: false, - type: 'PARAGRAPH', - text: { - de: 'Element in Array', - en: 'Element in Array' - }, - title: { - de: 'Array Element', - en: 'Array Element' - } - } as TextUIElement - }], - title: { - de: 'Array Element', - en: 'Array Element' - } - } as ArrayUIElement - }; - - case 'CustomUIElement': - return { - element: { - pattern_type: 'CustomUIElement', - required: false, - title: { - de: 'Benutzerdefiniertes Element', - en: 'Custom Element' - } - } as CustomUIElement - }; - - case 'CustomUIElement_SCANNER': - return { - element: { - pattern_type: 'CustomUIElement', - type: 'SCANNER', - id: `scanner-${uuidv4().substring(0, 8)}`, - related_custom_ui_element_id: `scanner-view-${uuidv4().substring(0, 8)}`, - display_position: 'RIGHT', - required: false, - title: { - de: 'Scanner (Doorbit Studio)', - en: 'Scanner (Doorbit Studio)' - }, - sub_flows: [ - // POI_PHOTO subflow - { - type: 'POI_PHOTO', - elements: [ - { - element: { - pattern_type: 'FileUIElement', - file_type: 'IMAGE', - title: {}, - min_count: 0, - max_count: 1, - required: false, - id_field_id: { - field_name: `building_poi_pictures_id_${uuidv4()}` - }, - caption_field_id: { - field_name: `building_poi_pictures_caption_${uuidv4()}` - }, - allowed_file_types: [] - } - } - ] - }, - // POI subflow - { - type: 'POI', - elements: [ - { - element: { - pattern_type: 'StringUIElement', - type: 'TEXT_AREA', - required: false, - field_id: { - field_name: `building_poi_text_${uuidv4()}` - }, - title: { - en: 'Description', - de: 'Beschreibung' - } - } - }, - { - element: { - pattern_type: 'ChipGroupUIElement', - required: false, - title: {}, - chips: [ - { - pattern_type: 'BooleanUIElement', - required: false, - icon: 'mdiAlert', - field_id: { - field_name: `building_poi_is_defect_${uuidv4()}` - }, - default_value: false, - title: { - en: 'Defect', - de: 'Mangel' - } - }, - { - pattern_type: 'BooleanUIElement', - required: false, - icon: 'mdiWrench', - field_id: { - field_name: `building_poi_is_todo_${uuidv4()}` - }, - default_value: false, - title: { - en: 'Todo', - de: 'Todo' - } - } - ] - } - }, - { - element: { - visibility_condition: { - field_id: { - field_name: 'building_poi_is_todo' - }, - op: 'eq', - value: true, - operator_type: 'RFO' - }, - pattern_type: 'DateUIElement', - type: 'YMD', - required: false, - field_id: { - field_name: 'building_poi_deadline_date' - }, - title: { - en: 'To be done until', - de: 'Zu erledigen bis' - }, - minimum: '-1Y' - } - }, - { - element: { - pattern_type: 'ChipGroupUIElement', - required: false, - title: {}, - chips: [ - { - visibility_condition: { - operator: 'AND', - operator_type: 'LO', - conditions: [ - { - field_id: { - field_name: 'building_poi_is_light_switch' - }, - op: 'ne', - value: true, - operator_type: 'RFO' - }, - { - field_id: { - field_name: 'building_poi_is_wall_socket' - }, - op: 'ne', - value: true, - operator_type: 'RFO' - }, - { - field_id: { - field_name: 'building_poi_is_lamp' - }, - op: 'ne', - value: true, - operator_type: 'RFO' - } - ] - }, - pattern_type: 'BooleanUIElement', - required: false, - icon: 'mdiRadiator', - field_id: { - field_name: 'building_poi_is_radiator' - }, - default_value: false, - title: { - en: 'Radiator', - de: 'Heizkörper' - } - }, - { - visibility_condition: { - operator: 'AND', - operator_type: 'LO', - conditions: [ - { - field_id: { - field_name: 'building_poi_is_radiator' - }, - op: 'ne', - value: true, - operator_type: 'RFO' - }, - { - field_id: { - field_name: 'building_poi_is_lamp' - }, - op: 'ne', - value: true, - operator_type: 'RFO' - } - ] - }, - pattern_type: 'BooleanUIElement', - required: false, - icon: 'mdiLightSwitch', - field_id: { - field_name: 'building_poi_is_light_switch' - }, - default_value: false, - title: { - en: 'Light switch', - de: 'Lichtschalter' - } - }, - { - visibility_condition: { - operator: 'AND', - operator_type: 'LO', - conditions: [ - { - field_id: { - field_name: 'building_poi_is_radiator' - }, - op: 'ne', - value: true, - operator_type: 'RFO' - }, - { - field_id: { - field_name: 'building_poi_is_lamp' - }, - op: 'ne', - value: true, - operator_type: 'RFO' - } - ] - }, - pattern_type: 'BooleanUIElement', - required: false, - icon: 'mdiPowerSocketDe', - field_id: { - field_name: 'building_poi_is_wall_socket' - }, - default_value: false, - title: { - en: 'Wall socket', - de: 'Steckdose' - } - }, - { - visibility_condition: { - operator: 'AND', - operator_type: 'LO', - conditions: [ - { - field_id: { - field_name: 'building_poi_is_radiator' - }, - op: 'ne', - value: true, - operator_type: 'RFO' - }, - { - field_id: { - field_name: 'building_poi_is_light_switch' - }, - op: 'ne', - value: true, - operator_type: 'RFO' - }, - { - field_id: { - field_name: 'building_poi_is_wall_socket' - }, - op: 'ne', - value: true, - operator_type: 'RFO' - } - ] - }, - pattern_type: 'BooleanUIElement', - required: false, - icon: 'mdiLightbulbOn', - field_id: { - field_name: 'building_poi_is_lamp' - }, - default_value: false, - title: { - en: 'Lamp', - de: 'Lampe' - } - } - ] - } - }, - { - element: { - pattern_type: 'SingleSelectionUIElement', - required: true, - field_id: { - field_name: 'building_poi_radiator_type' - }, - visibility_condition: { - field_id: { - field_name: 'building_poi_is_radiator' - }, - op: 'eq', - value: true, - operator_type: 'RFO' - }, - title: { - en: 'Type', - de: 'Typ' - }, - options: [ - { - key: 'RADIATOR', - label: { - en: 'Radiator', - de: 'Radiator' - }, - icon: 'mdiFence' - }, - { - key: 'PANEL_RADIATOR', - label: { - en: 'Panel radiator', - de: 'Plattenheizkörper' - }, - icon: 'mdiRadiatorDisabled' - }, - { - key: 'TUBE_RADIATOR', - label: { - en: 'Tube radiator', - de: 'Badheizkörper' - }, - icon: 'mdiHeatingCoil' - }, - { - key: 'CONVECTOR_HEATER', - label: { - en: 'Convector heater', - de: 'Konvektionsheizkörper' - }, - icon: 'mdiRadiator' - } - ] - } - }, - { - element: { - pattern_type: 'NumberUIElement', - type: 'INTEGER', - required: false, - field_id: { - field_name: 'building_poi_radiator_width' - }, - title: { - en: 'Width', - de: 'Breite' - }, - visibility_condition: { - field_id: { - field_name: 'building_poi_is_radiator' - }, - op: 'eq', - value: true, - operator_type: 'RFO' - } - } - }, - { - element: { - pattern_type: 'NumberUIElement', - type: 'INTEGER', - required: false, - field_id: { - field_name: 'building_poi_radiator_height' - }, - title: { - en: 'Height', - de: 'Höhe' - }, - visibility_condition: { - field_id: { - field_name: 'building_poi_is_radiator' - }, - op: 'eq', - value: true, - operator_type: 'RFO' - } - } - }, - { - element: { - pattern_type: 'NumberUIElement', - type: 'INTEGER', - required: false, - field_id: { - field_name: 'building_poi_radiator_depth' - }, - title: { - en: 'Depth', - de: 'Tiefe' - }, - visibility_condition: { - field_id: { - field_name: 'building_poi_is_radiator' - }, - op: 'eq', - value: true, - operator_type: 'RFO' - } - } - }, - { - element: { - pattern_type: 'FileUIElement', - file_type: 'IMAGE', - title: { - de: 'Fotos', - en: 'Photos' - }, - min_count: 0, - max_count: 3, - required: false, - id_field_id: { - field_name: `building_poi_pictures_id_${uuidv4()}` - }, - caption_field_id: { - field_name: `building_poi_pictures_caption_${uuidv4()}` - }, - allowed_file_types: [ - 'image/jpeg', - 'image/png', - 'image/webp', - 'image/tiff' - ] - } - } - ] - }, - // ROOM subflow - { - type: 'ROOM', - elements: [ - { - element: { - pattern_type: 'GroupUIElement', - required: false, - title: { - en: 'Heating information', - de: 'Angaben zur Beheizung' - }, - description: { - en: 'Select how the room is heated. A room can also be heated by adjacent rooms if the rooms are connected by a breakthrough.', - de: 'Wähle aus, wie der Raum beheizt ist. Ein Raum kann auch durch angrenzende Räume mitbeheizt sein, wenn die Räume mittels Durchbruch verbunden sind.' - }, - elements: [ - { - element: { - pattern_type: 'BooleanUIElement', - required: true, - field_id: { - field_name: 'is_room_heated' - }, - default_value: true, - title: { - en: 'Is room heated', - de: 'Ist der Raum beheizt?' - } - } - }, - { - element: { - visibility_condition: { - field_id: { - field_name: 'is_room_heated' - }, - op: 'eq', - value: true, - operator_type: 'RFO' - }, - pattern_type: 'ChipGroupUIElement', - required: false, - title: { - en: 'How is the room heated?', - de: 'Wie ist der Raum beheizt?' - }, - chips: [ - { - pattern_type: 'BooleanUIElement', - uuid: generateUUID(), - required: false, - field_id: { - field_name: 'has_radiators' - }, - default_value: false, - title: { - en: 'Radiators', - de: 'Heizkörper' - } - }, - { - pattern_type: 'BooleanUIElement', - uuid: generateUUID(), - required: false, - field_id: { - field_name: 'has_underfloor_heating' - }, - default_value: false, - title: { - en: 'Underfloor heating', - de: 'Fußbodenheizung' - } - }, - { - pattern_type: 'BooleanUIElement', - uuid: generateUUID(), - required: false, - field_id: { - field_name: 'has_fire_place' - }, - default_value: false, - title: { - en: 'Fire place', - de: 'Ofen' - } - }, - { - pattern_type: 'BooleanUIElement', - uuid: generateUUID(), - required: false, - field_id: { - field_name: 'has_indirect_heating' - }, - default_value: false, - title: { - en: 'Indirect heating', - de: 'Mitbeheizt' - } - } - ] - } - }, - { - element: { - pattern_type: 'SingleSelectionUIElement', - uuid: generateUUID(), - required: true, - field_id: { - field_name: 'room_temperature_type' - }, - visibility_condition: { - field_id: { - field_name: 'is_room_heated' - }, - op: 'eq', - value: true, - operator_type: 'RFO' - }, - title: { - en: 'Approx. room temperature', - de: 'Ungefähre Raumtemperatur' - }, - options: [ - { - key: 'LIVING_ROOM', - label: { - en: 'Living room (ca. 20°C)', - de: 'Wohnraum (ca. 20°C)' - } - }, - { - key: 'BATHROOM', - label: { - en: 'Bathroom (ca. 24°C)', - de: 'Badezimmer (ca. 24°C)' - } - }, - { - key: 'PASSAGE_ROOM', - label: { - en: 'Passage room (ca. 15°C)', - de: 'Flur / Abstellraum (ca. 15°C)' - } - } - ] - } - } - ] - } - }, - { - element: { - pattern_type: 'GroupUIElement', - uuid: generateUUID(), - required: false, - title: { - en: 'More details', - de: 'Weitere Angaben' - }, - elements: [ - { - element: { - pattern_type: 'SingleSelectionUIElement', - uuid: generateUUID(), - required: true, - field_id: { - field_name: 'room_category' - }, - title: { - en: 'Room category', - de: 'Raumkategorie' - }, - options: [ - { - key: 'LIVING_ROOM', - label: { - en: 'Living room', - de: 'Wohnzimmer' - } - }, - { - key: 'KITCHEN', - label: { - en: 'Kitchen', - de: 'Küche' - } - }, - { - key: 'BEDROOM', - label: { - en: 'Bedroom', - de: 'Schlaf-/Kinderzimmer' - } - }, - { - key: 'BATHROOM_GUEST_WC', - label: { - en: 'Bathroom/WC', - de: 'Badezimmer/WC' - } - } - ] - } - }, - { - element: { - pattern_type: 'StringUIElement', - uuid: generateUUID(), - type: 'TEXT', - required: false, - field_id: { - field_name: 'room_name' - }, - title: { - en: 'Custom room name', - de: 'Eigener Raumname' - }, - description: { - en: 'Will be generated from the room category if nothing is maintained.', - de: 'Wird aus der Raumkategorie erzeugt, wenn nichts gepflegt wird.' - } - } - } - ] - } - }, - { - element: { - pattern_type: 'GroupUIElement', - uuid: generateUUID(), - required: false, - title: { - en: 'Bill of quantities', - de: 'Massedaten' - }, - elements: [ - { - element: createBillOfQuantitiesElement() - } - ] - } - } - ] - }, - // ROOM_GROUP subflow - { - type: 'ROOM_GROUP', - elements: [ - { - element: { - pattern_type: 'GroupUIElement', - uuid: generateUUID(), - required: false, - title: { - en: 'Room group details', - de: 'Raumgruppen-Details' - }, - elements: [ - { - element: { - pattern_type: 'StringUIElement', - uuid: generateUUID(), - type: 'TEXT', - required: true, - field_id: { - field_name: 'room_group_name' - }, - title: { - en: 'Room group name', - de: 'Raumgruppenname' - } - } - }, - { - element: { - pattern_type: 'SingleSelectionUIElement', - uuid: generateUUID(), - required: true, - field_id: { - field_name: 'room_group_type' - }, - title: { - en: 'Room group type', - de: 'Raumgruppentyp' - }, - options: [ - { - key: 'APARTMENT', - label: { - en: 'Apartment', - de: 'Wohnung' - } - }, - { - key: 'FLOOR', - label: { - en: 'Floor', - de: 'Etage' - } - }, - { - key: 'WING', - label: { - en: 'Wing', - de: 'Gebäudeflügel' - } - } - ] - } - } - ] - } - }, - { - element: { - pattern_type: 'GroupUIElement', - uuid: generateUUID(), - required: false, - title: { - en: 'Bill of quantities', - de: 'Massedaten' - }, - elements: [ - { - element: createBillOfQuantitiesElement() - } - ] - } - } - ] - }, - // WINDOW subflow - { - type: 'WINDOW', - elements: [ - { - element: { - pattern_type: 'BooleanUIElement', - required: false, - default_value: false, - field_id: { - field_name: 'has_roller_shutters' - }, - title: { - en: 'Has roller shutters', - de: 'Rollladen vorhanden' - } - } - }, - { - element: { - visibility_condition: { - field_id: { - field_name: 'has_roller_shutters' - }, - op: 'eq', - value: true, - operator_type: 'RFO' - }, - pattern_type: 'NumberUIElement', - type: 'DOUBLE', - required: false, - default: 0.25, - field_id: { - field_name: 'roller_shutters_height' - }, - title: { - en: 'Roller shutters height', - de: 'Höhe des Rollladenkastens' - }, - icon: 'mdiArrowExpandVertical' - } - }, - { - element: { - pattern_type: 'BooleanUIElement', - required: false, - default_value: false, - field_id: { - field_name: 'has_heizkoerpernische' - }, - title: { - en: 'Has radiator niche', - de: 'Heizkörpernische vorhanden' - } - } - }, - { - element: { - visibility_condition: { - field_id: { - field_name: 'has_heizkoerpernische' - }, - op: 'eq', - value: true, - operator_type: 'RFO' - }, - pattern_type: 'NumberUIElement', - type: 'DOUBLE', - required: false, - field_id: { - field_name: 'heizkoerpernischen_height' - }, - title: { - en: 'Radiator niche height', - de: 'Höhe der HZK-Nische' - }, - icon: 'mdiArrowExpandVertical' - } - }, - { - element: { - pattern_type: 'SingleSelectionUIElement', - required: true, - field_id: { - field_name: 'window_glazing_type' - }, - title: { - en: 'Window glazing', - de: 'Fensterverglasung' - }, - options: [ - { - key: 'SINGLE', - label: { - en: 'Single glazing', - de: '1-fach' - } - }, - { - key: 'DOUBLE', - label: { - en: 'Double glazing', - de: '2-fach' - } - }, - { - key: 'TRIPLE', - label: { - en: 'Triple glazing', - de: '3-fach' - } - } - ] - } - } - ] - } - ] - } as CustomUIElement - }; - - case 'CustomUIElement_ADDRESS': - return { - element: { - pattern_type: 'CustomUIElement', - type: 'ADDRESS', - id: `address-${uuidv4().substring(0, 8)}`, - required: false, - title: { - de: 'Adresse ermitteln', - en: 'Determine address' - }, - short_title: { - de: 'Adresse', - en: 'Address' - }, - elements: [ - { - element: { - pattern_type: 'BooleanUIElement', - required: false, - field_id: { - field_name: 'address_show_obfuscated_location' - }, - default_value: false, - title: { - de: 'Adresse verbergen?', - en: 'Hide address?' - }, - description: { - de: 'Wird die Adresse verborgen, ist sie nicht öffentlich sichtbar. Stattdessen wird ein ungefährer Standort präsentiert.', - en: 'If the address is hidden, it won\'t be publicly visible. Instead, an approximate location will be displayed.' - } - } - } - ] - } as CustomUIElement - }; - - case 'CustomUIElement_LOCATION': - return { - element: { - pattern_type: 'CustomUIElement', - type: 'LOCATION', - id: `location-${uuidv4().substring(0, 8)}`, - icon: 'mdiMapMarker', - required: false, - short_title: { - de: 'Lage', - en: 'Location' - }, - title: { - de: 'Standort', - en: 'Location' - }, - elements: [] - } as CustomUIElement - }; - - case 'CustomUIElement_ADMIN_BOUNDARY': - return { - element: { - pattern_type: 'CustomUIElement', - type: 'ADMIN_BOUNDARY', - id: `admin-boundary-${uuidv4().substring(0, 8)}`, - icon: 'mdiHomeGroup', - required: false, - short_title: { - de: 'Nachbarschaft', - en: 'Administrative boundary' - }, - title: { - de: 'Umgebungsinfos', - en: 'Environment information' - }, - elements: [] - } as CustomUIElement - }; - - case 'ChipGroupUIElement': { - // Erstelle BooleanUIElements direkt, ohne PatternLibraryElement-Wrapper - const bool1: BooleanUIElement = { - pattern_type: 'BooleanUIElement', - required: false, - field_id: { field_name: `chip_${uuidv4()}` }, - title: { de: 'Option 1', en: 'Option 1' }, - description: { de: 'Erste Option', en: 'First option' } - }; - - const bool2: BooleanUIElement = { - pattern_type: 'BooleanUIElement', - required: false, - field_id: { field_name: `chip_${uuidv4()}` }, - title: { de: 'Option 2', en: 'Option 2' }, - description: { de: 'Zweite Option', en: 'Second option' } - }; - - return { - element: { - pattern_type: 'ChipGroupUIElement', - required: false, - title: { - de: 'Chip-Gruppe Element', - en: 'Chip Group Element' - }, - // ChipGroupUIElement.chips erwartet BooleanUIElement[], nicht PatternLibraryElement[] - chips: [bool1, bool2] - } as ChipGroupUIElement - }; - } - - case 'StringUIElement': - return { - element: { - pattern_type: 'StringUIElement', - required: false, - type: 'TEXT', - field_id: { - field_name: `string_field_${uuidv4()}` - }, - length_minimum: 0, - length_maximum: 100, - title: { - de: 'String Element', - en: 'String Element' - } - } as StringUIElement - }; - - case 'ImageGalleryUIElement': - return { - element: { - pattern_type: 'ImageGalleryUIElement', - required: false, - preferred_size: 'M', - display_position: 'LEFT', - id_field_value: { field_id: { field_name: '' } }, - title: { - de: 'Bildergalerie', - en: 'Image Gallery' - } - } as ImageGalleryUIElement - }; - - case 'FieldTextUIElement': - return { - element: { - pattern_type: 'FieldTextUIElement', - required: false, - type: 'PARAGRAPH', - field_value: { field_id: { field_name: '' } }, - title: { - de: 'Feldtext', - en: 'Field Text' - } - } as FieldTextUIElement - }; - - case 'TableUIElement': - return { - element: { - pattern_type: 'TableUIElement', - required: false, - type: 'TABLE', - columns: [], - title: { - de: 'Tabelle', - en: 'Table' - } - } as TableUIElement - }; - - case 'ContactUIElement': - return { - element: { - pattern_type: 'ContactUIElement', - required: false, - identity_type: 'USER_ID', - show_name: true, - show_picture: true, - show_details: false, - title: { - de: 'Kontakt', - en: 'Contact' - } - } as ContactUIElement - }; - - default: - // Fallback auf TextUIElement - return { - element: { - pattern_type: 'TextUIElement', - required: false, - type: 'PARAGRAPH', - text: { - de: 'Beispieltext', - en: 'Example text' - }, - title: { - de: 'Text Element', - en: 'Text Element' - } - } as TextUIElement - }; - } -}; +// createElement ist in ./utils/elementFactory ausgelagert (UI-frei, isoliert testbar). const AppContent: React.FC = () => { const { state, dispatch } = useEditor(); diff --git a/src/utils/elementFactory.test.ts b/src/utils/elementFactory.test.ts new file mode 100644 index 0000000..48f6306 --- /dev/null +++ b/src/utils/elementFactory.test.ts @@ -0,0 +1,32 @@ +import { createElement } from './elementFactory'; + +describe('createElement – CustomUIElement-Titel', () => { + const customTypes = [ + 'CustomUIElement', + 'CustomUIElement_SCANNER', + 'CustomUIElement_ADDRESS', + 'CustomUIElement_LOCATION', + 'CustomUIElement_ADMIN_BOUNDARY' + ]; + + it.each(customTypes)( + 'vergibt für %s keinen Platzhalter-title (kein Schema-Drift zu portal)', + (type) => { + const el = createElement(type).element as any; + expect(el.pattern_type).toBe('CustomUIElement'); + expect(el.title).toBeUndefined(); + } + ); + + it('behält das aussagekräftige short_title für ADDRESS/LOCATION/ADMIN_BOUNDARY', () => { + ['CustomUIElement_ADDRESS', 'CustomUIElement_LOCATION', 'CustomUIElement_ADMIN_BOUNDARY'].forEach((type) => { + const el = createElement(type).element as any; + expect(el.short_title).toBeDefined(); + }); + }); + + it('setzt bei Nicht-Custom-Elementen weiterhin einen title (Regressionsschutz)', () => { + const text = createElement('TextUIElement').element as any; + expect(text.title).toBeDefined(); + }); +}); diff --git a/src/utils/elementFactory.ts b/src/utils/elementFactory.ts new file mode 100644 index 0000000..b37898b --- /dev/null +++ b/src/utils/elementFactory.ts @@ -0,0 +1,1447 @@ +import { v4 as uuidv4 } from 'uuid'; +import { generateUUID } from './uuidUtils'; +import { PatternLibraryElement } from '../models/listingFlow'; +import { + TextUIElement, + BooleanUIElement, + SingleSelectionUIElement, + NumberUIElement, + DateUIElement, + FileUIElement, + GroupUIElement, + ArrayUIElement, + CustomUIElement, + ChipGroupUIElement, + StringUIElement, + KeyValueListUIElement, + ImageGalleryUIElement, + FieldTextUIElement, + TableUIElement, + ContactUIElement +} from '../models/uiElements'; + +/** + * Erzeugt ein KeyValueListUIElement für Massedaten (Bill of quantities) + * Diese Funktion wird verwendet, um automatisch das KeyValueListUIElement in ROOM und ROOM_GROUP Subflows einzufügen + * @returns Ein KeyValueListUIElement mit vordefinierten Massedaten-Feldern + */ +const createBillOfQuantitiesElement = (): KeyValueListUIElement => { + return { + pattern_type: 'KeyValueListUIElement', + type: 'TABLE', + required: false, + items: [ + { + icon: 'mdiFloorPlan', + key: { + en: 'Nettogrundfläche (NGF)', + de: 'Nettogrundfläche (NGF)' + }, + field_value: { + field_id: { + field_name: `netto_raumflaeche_${uuidv4().substring(0, 8)}` + } + } + }, + { + icon: 'mdiCube', + key: { + en: 'Netto-Rauminhalt (NRI)', + de: 'Netto-Rauminhalt (NRI)' + }, + field_value: { + field_id: { + field_name: `netto_rauminhalt_${uuidv4().substring(0, 8)}` + } + } + }, + { + icon: 'mdiFloorPlan', + key: { + en: 'Bruttogrundfläche (BGF)', + de: 'Bruttogrundfläche (BGF)' + }, + field_value: { + field_id: { + field_name: `brutto_grundflaeche_${uuidv4().substring(0, 8)}` + } + } + }, + { + icon: 'mdiCube', + key: { + en: 'Brutto-Rauminhalt (BRI)', + de: 'Brutto-Rauminhalt (BRI)' + }, + field_value: { + field_id: { + field_name: `brutto_rauminhalt_${uuidv4().substring(0, 8)}` + } + } + }, + { + icon: 'mdiHeatingCoil', + key: { + en: 'Technikfläche (TF)', + de: 'Technikfläche (TF)' + }, + field_value: { + field_id: { + field_name: `technik_flaeche_${uuidv4().substring(0, 8)}` + } + } + }, + { + icon: 'mdiWalk', + key: { + en: 'Verkehrsfläche (VF)', + de: 'Verkehrsfläche (VF)' + }, + field_value: { + field_id: { + field_name: `verkehrs_flaeche_${uuidv4().substring(0, 8)}` + } + } + }, + { + icon: 'mdiHumanMaleHeight', + key: { + en: 'Room height', + de: 'Raumhöhe' + }, + field_value: { + field_id: { + field_name: `room_height_${uuidv4().substring(0, 8)}` + } + } + }, + { + icon: 'mdiFloorPlan', + key: { + en: 'Room perimeter', + de: 'Raumumfang' + }, + field_value: { + field_id: { + field_name: `slab_perimeter_${uuidv4().substring(0, 8)}` + } + } + }, + { + icon: 'mdiWall', + key: { + en: 'Inside area (net)', + de: 'Innenfläche (netto)' + }, + field_value: { + field_id: { + field_name: `inside_area_net_${uuidv4().substring(0, 8)}` + } + } + }, + { + icon: 'mdiWall', + key: { + en: 'Inside area (gross)', + de: 'Innenfläche (brutto)' + }, + field_value: { + field_id: { + field_name: `inside_area_gross_${uuidv4().substring(0, 8)}` + } + } + }, + { + icon: 'mdiWall', + key: { + en: 'Outside area (net)', + de: 'Außenfläche (netto)' + }, + field_value: { + field_id: { + field_name: `outside_area_net_${uuidv4().substring(0, 8)}` + } + } + }, + { + icon: 'mdiWall', + key: { + en: 'Outside area (gross)', + de: 'Außenfläche (brutto)' + }, + field_value: { + field_id: { + field_name: `outside_area_gross_${uuidv4().substring(0, 8)}` + } + } + } + ] + }; +}; + +/** + * Factory für neue Flow-Elemente anhand des Palette-Typs. + * + * Bewusst UI-frei (keine styled-components/MUI-Imports), damit die reine + * Element-Erzeugung isoliert testbar bleibt und den Editor-Kern nicht an + * CRA/Craco-Spezifika bindet. + */ +export const createElement = (type: string): PatternLibraryElement => { + switch (type) { + case 'TextUIElement': + return { + element: { + pattern_type: 'TextUIElement', + required: false, + type: 'PARAGRAPH', + text: { + de: 'Beispieltext', + en: 'Example text' + }, + title: { + de: 'Text Element', + en: 'Text Element' + } + } as TextUIElement + }; + + case 'BooleanUIElement': + return { + element: { + pattern_type: 'BooleanUIElement', + required: false, + field_id: { + field_name: `boolean_field_${uuidv4()}` + }, + default_value: false, + title: { + de: 'Boolean Element', + en: 'Boolean Element' + } + } as BooleanUIElement + }; + + case 'SingleSelectionUIElement': + return { + element: { + pattern_type: 'SingleSelectionUIElement', + required: false, + field_id: { + field_name: `selection_field_${uuidv4()}` + }, + type: 'DROPDOWN', + options: [ + { key: uuidv4(), label: { de: 'Option 1', en: 'Option 1' } }, + { key: uuidv4(), label: { de: 'Option 2', en: 'Option 2' } }, + { key: uuidv4(), label: { de: 'Option 3', en: 'Option 3' } } + ], + title: { + de: 'Auswahl Element', + en: 'Selection Element' + } + } as SingleSelectionUIElement + }; + + case 'NumberUIElement': + return { + element: { + pattern_type: 'NumberUIElement', + required: false, + field_id: { + field_name: `number_field_${uuidv4()}` + }, + type: 'INTEGER', + minimum: 0, + maximum: 100, + title: { + de: 'Nummer Element', + en: 'Number Element' + } + } as NumberUIElement + }; + + case 'DateUIElement': + return { + element: { + pattern_type: 'DateUIElement', + required: false, + field_id: { + field_name: `date_field_${uuidv4()}` + }, + // Standardmäßig Jahresauswahl ('Y') entsprechend enion_esg.json + type: 'Y', + title: { + de: 'Datum Element', + en: 'Date Element' + } + } as DateUIElement + }; + + case 'FileUIElement': + const fileUuid = uuidv4(); + return { + element: { + pattern_type: 'FileUIElement', + required: false, + file_type: 'IMAGE', + allowed_file_types: ['image/jpeg', 'image/png'], + // KRITISCH: field_id für eindeutige Identifikation des Elements + field_id: { + field_name: `fileuielement_${fileUuid}` + }, + // KRITISCH: id_field_id für Speicherort der Datei-IDs (eindeutig mit UUID) + id_field_id: { + field_name: `file_images_id_${uuidv4()}` + }, + // EMPFOHLEN: caption_field_id für Bildunterschriften (eindeutig mit UUID) + caption_field_id: { + field_name: `file_images_caption_${uuidv4()}` + }, + // EMPFOHLEN: min_count und max_count definieren + min_count: 0, + max_count: 10, + title: { + de: 'Datei Element', + en: 'File Element' + }, + description: { + de: '', + en: '' + } + } as FileUIElement + }; + + case 'GroupUIElement': + return { + element: { + pattern_type: 'GroupUIElement', + required: false, + isCollapsible: false, + elements: [], // Leere Elemente-Liste + title: { + de: 'Gruppen Element', + en: 'Group Element' + } + } as GroupUIElement + }; + + case 'ArrayUIElement': + return { + element: { + pattern_type: 'ArrayUIElement', + required: false, + field_id: { + field_name: `array_field_${uuidv4()}` + }, + min_count: 0, // Korrigiert: min_count statt min_items + max_count: 10, // Korrigiert: max_count statt max_items + elements: [{ // Korrigiert: elements statt item_template + element: { + pattern_type: 'TextUIElement', + required: false, + type: 'PARAGRAPH', + text: { + de: 'Element in Array', + en: 'Element in Array' + }, + title: { + de: 'Array Element', + en: 'Array Element' + } + } as TextUIElement + }], + title: { + de: 'Array Element', + en: 'Array Element' + } + } as ArrayUIElement + }; + + case 'CustomUIElement': + // Kein Default-`title`: CustomUIElements werden von portal-applications mit + // ihrer eigenen (i18n-)Überschrift gerendert und das Schema kennt kein `title` + // für CustomUIElement. Ein Platzhalter-Titel würde als sinnlose Überschrift + // in den Flow durchschlagen (Schema-Drift-Bug). + return { + element: { + pattern_type: 'CustomUIElement', + required: false + } as CustomUIElement + }; + + case 'CustomUIElement_SCANNER': + return { + element: { + pattern_type: 'CustomUIElement', + type: 'SCANNER', + id: `scanner-${uuidv4().substring(0, 8)}`, + related_custom_ui_element_id: `scanner-view-${uuidv4().substring(0, 8)}`, + display_position: 'RIGHT', + required: false, + // Kein Default-`title`: portal-applications rendert den Scanner (Doorbit + // Studio) mit eigener Überschrift; ein Platzhalter-Titel würde als sinnlose + // Überschrift durchschlagen (Schema-Drift, siehe CustomUIElement oben). + sub_flows: [ + // POI_PHOTO subflow + { + type: 'POI_PHOTO', + elements: [ + { + element: { + pattern_type: 'FileUIElement', + file_type: 'IMAGE', + title: {}, + min_count: 0, + max_count: 1, + required: false, + id_field_id: { + field_name: `building_poi_pictures_id_${uuidv4()}` + }, + caption_field_id: { + field_name: `building_poi_pictures_caption_${uuidv4()}` + }, + allowed_file_types: [] + } + } + ] + }, + // POI subflow + { + type: 'POI', + elements: [ + { + element: { + pattern_type: 'StringUIElement', + type: 'TEXT_AREA', + required: false, + field_id: { + field_name: `building_poi_text_${uuidv4()}` + }, + title: { + en: 'Description', + de: 'Beschreibung' + } + } + }, + { + element: { + pattern_type: 'ChipGroupUIElement', + required: false, + title: {}, + chips: [ + { + pattern_type: 'BooleanUIElement', + required: false, + icon: 'mdiAlert', + field_id: { + field_name: `building_poi_is_defect_${uuidv4()}` + }, + default_value: false, + title: { + en: 'Defect', + de: 'Mangel' + } + }, + { + pattern_type: 'BooleanUIElement', + required: false, + icon: 'mdiWrench', + field_id: { + field_name: `building_poi_is_todo_${uuidv4()}` + }, + default_value: false, + title: { + en: 'Todo', + de: 'Todo' + } + } + ] + } + }, + { + element: { + visibility_condition: { + field_id: { + field_name: 'building_poi_is_todo' + }, + op: 'eq', + value: true, + operator_type: 'RFO' + }, + pattern_type: 'DateUIElement', + type: 'YMD', + required: false, + field_id: { + field_name: 'building_poi_deadline_date' + }, + title: { + en: 'To be done until', + de: 'Zu erledigen bis' + }, + minimum: '-1Y' + } + }, + { + element: { + pattern_type: 'ChipGroupUIElement', + required: false, + title: {}, + chips: [ + { + visibility_condition: { + operator: 'AND', + operator_type: 'LO', + conditions: [ + { + field_id: { + field_name: 'building_poi_is_light_switch' + }, + op: 'ne', + value: true, + operator_type: 'RFO' + }, + { + field_id: { + field_name: 'building_poi_is_wall_socket' + }, + op: 'ne', + value: true, + operator_type: 'RFO' + }, + { + field_id: { + field_name: 'building_poi_is_lamp' + }, + op: 'ne', + value: true, + operator_type: 'RFO' + } + ] + }, + pattern_type: 'BooleanUIElement', + required: false, + icon: 'mdiRadiator', + field_id: { + field_name: 'building_poi_is_radiator' + }, + default_value: false, + title: { + en: 'Radiator', + de: 'Heizkörper' + } + }, + { + visibility_condition: { + operator: 'AND', + operator_type: 'LO', + conditions: [ + { + field_id: { + field_name: 'building_poi_is_radiator' + }, + op: 'ne', + value: true, + operator_type: 'RFO' + }, + { + field_id: { + field_name: 'building_poi_is_lamp' + }, + op: 'ne', + value: true, + operator_type: 'RFO' + } + ] + }, + pattern_type: 'BooleanUIElement', + required: false, + icon: 'mdiLightSwitch', + field_id: { + field_name: 'building_poi_is_light_switch' + }, + default_value: false, + title: { + en: 'Light switch', + de: 'Lichtschalter' + } + }, + { + visibility_condition: { + operator: 'AND', + operator_type: 'LO', + conditions: [ + { + field_id: { + field_name: 'building_poi_is_radiator' + }, + op: 'ne', + value: true, + operator_type: 'RFO' + }, + { + field_id: { + field_name: 'building_poi_is_lamp' + }, + op: 'ne', + value: true, + operator_type: 'RFO' + } + ] + }, + pattern_type: 'BooleanUIElement', + required: false, + icon: 'mdiPowerSocketDe', + field_id: { + field_name: 'building_poi_is_wall_socket' + }, + default_value: false, + title: { + en: 'Wall socket', + de: 'Steckdose' + } + }, + { + visibility_condition: { + operator: 'AND', + operator_type: 'LO', + conditions: [ + { + field_id: { + field_name: 'building_poi_is_radiator' + }, + op: 'ne', + value: true, + operator_type: 'RFO' + }, + { + field_id: { + field_name: 'building_poi_is_light_switch' + }, + op: 'ne', + value: true, + operator_type: 'RFO' + }, + { + field_id: { + field_name: 'building_poi_is_wall_socket' + }, + op: 'ne', + value: true, + operator_type: 'RFO' + } + ] + }, + pattern_type: 'BooleanUIElement', + required: false, + icon: 'mdiLightbulbOn', + field_id: { + field_name: 'building_poi_is_lamp' + }, + default_value: false, + title: { + en: 'Lamp', + de: 'Lampe' + } + } + ] + } + }, + { + element: { + pattern_type: 'SingleSelectionUIElement', + required: true, + field_id: { + field_name: 'building_poi_radiator_type' + }, + visibility_condition: { + field_id: { + field_name: 'building_poi_is_radiator' + }, + op: 'eq', + value: true, + operator_type: 'RFO' + }, + title: { + en: 'Type', + de: 'Typ' + }, + options: [ + { + key: 'RADIATOR', + label: { + en: 'Radiator', + de: 'Radiator' + }, + icon: 'mdiFence' + }, + { + key: 'PANEL_RADIATOR', + label: { + en: 'Panel radiator', + de: 'Plattenheizkörper' + }, + icon: 'mdiRadiatorDisabled' + }, + { + key: 'TUBE_RADIATOR', + label: { + en: 'Tube radiator', + de: 'Badheizkörper' + }, + icon: 'mdiHeatingCoil' + }, + { + key: 'CONVECTOR_HEATER', + label: { + en: 'Convector heater', + de: 'Konvektionsheizkörper' + }, + icon: 'mdiRadiator' + } + ] + } + }, + { + element: { + pattern_type: 'NumberUIElement', + type: 'INTEGER', + required: false, + field_id: { + field_name: 'building_poi_radiator_width' + }, + title: { + en: 'Width', + de: 'Breite' + }, + visibility_condition: { + field_id: { + field_name: 'building_poi_is_radiator' + }, + op: 'eq', + value: true, + operator_type: 'RFO' + } + } + }, + { + element: { + pattern_type: 'NumberUIElement', + type: 'INTEGER', + required: false, + field_id: { + field_name: 'building_poi_radiator_height' + }, + title: { + en: 'Height', + de: 'Höhe' + }, + visibility_condition: { + field_id: { + field_name: 'building_poi_is_radiator' + }, + op: 'eq', + value: true, + operator_type: 'RFO' + } + } + }, + { + element: { + pattern_type: 'NumberUIElement', + type: 'INTEGER', + required: false, + field_id: { + field_name: 'building_poi_radiator_depth' + }, + title: { + en: 'Depth', + de: 'Tiefe' + }, + visibility_condition: { + field_id: { + field_name: 'building_poi_is_radiator' + }, + op: 'eq', + value: true, + operator_type: 'RFO' + } + } + }, + { + element: { + pattern_type: 'FileUIElement', + file_type: 'IMAGE', + title: { + de: 'Fotos', + en: 'Photos' + }, + min_count: 0, + max_count: 3, + required: false, + id_field_id: { + field_name: `building_poi_pictures_id_${uuidv4()}` + }, + caption_field_id: { + field_name: `building_poi_pictures_caption_${uuidv4()}` + }, + allowed_file_types: [ + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/tiff' + ] + } + } + ] + }, + // ROOM subflow + { + type: 'ROOM', + elements: [ + { + element: { + pattern_type: 'GroupUIElement', + required: false, + title: { + en: 'Heating information', + de: 'Angaben zur Beheizung' + }, + description: { + en: 'Select how the room is heated. A room can also be heated by adjacent rooms if the rooms are connected by a breakthrough.', + de: 'Wähle aus, wie der Raum beheizt ist. Ein Raum kann auch durch angrenzende Räume mitbeheizt sein, wenn die Räume mittels Durchbruch verbunden sind.' + }, + elements: [ + { + element: { + pattern_type: 'BooleanUIElement', + required: true, + field_id: { + field_name: 'is_room_heated' + }, + default_value: true, + title: { + en: 'Is room heated', + de: 'Ist der Raum beheizt?' + } + } + }, + { + element: { + visibility_condition: { + field_id: { + field_name: 'is_room_heated' + }, + op: 'eq', + value: true, + operator_type: 'RFO' + }, + pattern_type: 'ChipGroupUIElement', + required: false, + title: { + en: 'How is the room heated?', + de: 'Wie ist der Raum beheizt?' + }, + chips: [ + { + pattern_type: 'BooleanUIElement', + uuid: generateUUID(), + required: false, + field_id: { + field_name: 'has_radiators' + }, + default_value: false, + title: { + en: 'Radiators', + de: 'Heizkörper' + } + }, + { + pattern_type: 'BooleanUIElement', + uuid: generateUUID(), + required: false, + field_id: { + field_name: 'has_underfloor_heating' + }, + default_value: false, + title: { + en: 'Underfloor heating', + de: 'Fußbodenheizung' + } + }, + { + pattern_type: 'BooleanUIElement', + uuid: generateUUID(), + required: false, + field_id: { + field_name: 'has_fire_place' + }, + default_value: false, + title: { + en: 'Fire place', + de: 'Ofen' + } + }, + { + pattern_type: 'BooleanUIElement', + uuid: generateUUID(), + required: false, + field_id: { + field_name: 'has_indirect_heating' + }, + default_value: false, + title: { + en: 'Indirect heating', + de: 'Mitbeheizt' + } + } + ] + } + }, + { + element: { + pattern_type: 'SingleSelectionUIElement', + uuid: generateUUID(), + required: true, + field_id: { + field_name: 'room_temperature_type' + }, + visibility_condition: { + field_id: { + field_name: 'is_room_heated' + }, + op: 'eq', + value: true, + operator_type: 'RFO' + }, + title: { + en: 'Approx. room temperature', + de: 'Ungefähre Raumtemperatur' + }, + options: [ + { + key: 'LIVING_ROOM', + label: { + en: 'Living room (ca. 20°C)', + de: 'Wohnraum (ca. 20°C)' + } + }, + { + key: 'BATHROOM', + label: { + en: 'Bathroom (ca. 24°C)', + de: 'Badezimmer (ca. 24°C)' + } + }, + { + key: 'PASSAGE_ROOM', + label: { + en: 'Passage room (ca. 15°C)', + de: 'Flur / Abstellraum (ca. 15°C)' + } + } + ] + } + } + ] + } + }, + { + element: { + pattern_type: 'GroupUIElement', + uuid: generateUUID(), + required: false, + title: { + en: 'More details', + de: 'Weitere Angaben' + }, + elements: [ + { + element: { + pattern_type: 'SingleSelectionUIElement', + uuid: generateUUID(), + required: true, + field_id: { + field_name: 'room_category' + }, + title: { + en: 'Room category', + de: 'Raumkategorie' + }, + options: [ + { + key: 'LIVING_ROOM', + label: { + en: 'Living room', + de: 'Wohnzimmer' + } + }, + { + key: 'KITCHEN', + label: { + en: 'Kitchen', + de: 'Küche' + } + }, + { + key: 'BEDROOM', + label: { + en: 'Bedroom', + de: 'Schlaf-/Kinderzimmer' + } + }, + { + key: 'BATHROOM_GUEST_WC', + label: { + en: 'Bathroom/WC', + de: 'Badezimmer/WC' + } + } + ] + } + }, + { + element: { + pattern_type: 'StringUIElement', + uuid: generateUUID(), + type: 'TEXT', + required: false, + field_id: { + field_name: 'room_name' + }, + title: { + en: 'Custom room name', + de: 'Eigener Raumname' + }, + description: { + en: 'Will be generated from the room category if nothing is maintained.', + de: 'Wird aus der Raumkategorie erzeugt, wenn nichts gepflegt wird.' + } + } + } + ] + } + }, + { + element: { + pattern_type: 'GroupUIElement', + uuid: generateUUID(), + required: false, + title: { + en: 'Bill of quantities', + de: 'Massedaten' + }, + elements: [ + { + element: createBillOfQuantitiesElement() + } + ] + } + } + ] + }, + // ROOM_GROUP subflow + { + type: 'ROOM_GROUP', + elements: [ + { + element: { + pattern_type: 'GroupUIElement', + uuid: generateUUID(), + required: false, + title: { + en: 'Room group details', + de: 'Raumgruppen-Details' + }, + elements: [ + { + element: { + pattern_type: 'StringUIElement', + uuid: generateUUID(), + type: 'TEXT', + required: true, + field_id: { + field_name: 'room_group_name' + }, + title: { + en: 'Room group name', + de: 'Raumgruppenname' + } + } + }, + { + element: { + pattern_type: 'SingleSelectionUIElement', + uuid: generateUUID(), + required: true, + field_id: { + field_name: 'room_group_type' + }, + title: { + en: 'Room group type', + de: 'Raumgruppentyp' + }, + options: [ + { + key: 'APARTMENT', + label: { + en: 'Apartment', + de: 'Wohnung' + } + }, + { + key: 'FLOOR', + label: { + en: 'Floor', + de: 'Etage' + } + }, + { + key: 'WING', + label: { + en: 'Wing', + de: 'Gebäudeflügel' + } + } + ] + } + } + ] + } + }, + { + element: { + pattern_type: 'GroupUIElement', + uuid: generateUUID(), + required: false, + title: { + en: 'Bill of quantities', + de: 'Massedaten' + }, + elements: [ + { + element: createBillOfQuantitiesElement() + } + ] + } + } + ] + }, + // WINDOW subflow + { + type: 'WINDOW', + elements: [ + { + element: { + pattern_type: 'BooleanUIElement', + required: false, + default_value: false, + field_id: { + field_name: 'has_roller_shutters' + }, + title: { + en: 'Has roller shutters', + de: 'Rollladen vorhanden' + } + } + }, + { + element: { + visibility_condition: { + field_id: { + field_name: 'has_roller_shutters' + }, + op: 'eq', + value: true, + operator_type: 'RFO' + }, + pattern_type: 'NumberUIElement', + type: 'DOUBLE', + required: false, + default: 0.25, + field_id: { + field_name: 'roller_shutters_height' + }, + title: { + en: 'Roller shutters height', + de: 'Höhe des Rollladenkastens' + }, + icon: 'mdiArrowExpandVertical' + } + }, + { + element: { + pattern_type: 'BooleanUIElement', + required: false, + default_value: false, + field_id: { + field_name: 'has_heizkoerpernische' + }, + title: { + en: 'Has radiator niche', + de: 'Heizkörpernische vorhanden' + } + } + }, + { + element: { + visibility_condition: { + field_id: { + field_name: 'has_heizkoerpernische' + }, + op: 'eq', + value: true, + operator_type: 'RFO' + }, + pattern_type: 'NumberUIElement', + type: 'DOUBLE', + required: false, + field_id: { + field_name: 'heizkoerpernischen_height' + }, + title: { + en: 'Radiator niche height', + de: 'Höhe der HZK-Nische' + }, + icon: 'mdiArrowExpandVertical' + } + }, + { + element: { + pattern_type: 'SingleSelectionUIElement', + required: true, + field_id: { + field_name: 'window_glazing_type' + }, + title: { + en: 'Window glazing', + de: 'Fensterverglasung' + }, + options: [ + { + key: 'SINGLE', + label: { + en: 'Single glazing', + de: '1-fach' + } + }, + { + key: 'DOUBLE', + label: { + en: 'Double glazing', + de: '2-fach' + } + }, + { + key: 'TRIPLE', + label: { + en: 'Triple glazing', + de: '3-fach' + } + } + ] + } + } + ] + } + ] + } as CustomUIElement + }; + + case 'CustomUIElement_ADDRESS': + return { + element: { + pattern_type: 'CustomUIElement', + type: 'ADDRESS', + id: `address-${uuidv4().substring(0, 8)}`, + required: false, + // Kein Default-`title` (portal rendert eigene Überschrift); nur das für die + // Navigation genutzte short_title setzen. + short_title: { + de: 'Adresse', + en: 'Address' + }, + elements: [ + { + element: { + pattern_type: 'BooleanUIElement', + required: false, + field_id: { + field_name: 'address_show_obfuscated_location' + }, + default_value: false, + title: { + de: 'Adresse verbergen?', + en: 'Hide address?' + }, + description: { + de: 'Wird die Adresse verborgen, ist sie nicht öffentlich sichtbar. Stattdessen wird ein ungefährer Standort präsentiert.', + en: 'If the address is hidden, it won\'t be publicly visible. Instead, an approximate location will be displayed.' + } + } + } + ] + } as CustomUIElement + }; + + case 'CustomUIElement_LOCATION': + return { + element: { + pattern_type: 'CustomUIElement', + type: 'LOCATION', + id: `location-${uuidv4().substring(0, 8)}`, + icon: 'mdiMapMarker', + required: false, + // Kein Default-`title` (portal rendert eigene Überschrift); nur short_title. + short_title: { + de: 'Lage', + en: 'Location' + }, + elements: [] + } as CustomUIElement + }; + + case 'CustomUIElement_ADMIN_BOUNDARY': + return { + element: { + pattern_type: 'CustomUIElement', + type: 'ADMIN_BOUNDARY', + id: `admin-boundary-${uuidv4().substring(0, 8)}`, + icon: 'mdiHomeGroup', + required: false, + // Kein Default-`title` (portal rendert eigene Überschrift); nur short_title. + short_title: { + de: 'Nachbarschaft', + en: 'Administrative boundary' + }, + elements: [] + } as CustomUIElement + }; + + case 'ChipGroupUIElement': { + // Erstelle BooleanUIElements direkt, ohne PatternLibraryElement-Wrapper + const bool1: BooleanUIElement = { + pattern_type: 'BooleanUIElement', + required: false, + field_id: { field_name: `chip_${uuidv4()}` }, + title: { de: 'Option 1', en: 'Option 1' }, + description: { de: 'Erste Option', en: 'First option' } + }; + + const bool2: BooleanUIElement = { + pattern_type: 'BooleanUIElement', + required: false, + field_id: { field_name: `chip_${uuidv4()}` }, + title: { de: 'Option 2', en: 'Option 2' }, + description: { de: 'Zweite Option', en: 'Second option' } + }; + + return { + element: { + pattern_type: 'ChipGroupUIElement', + required: false, + title: { + de: 'Chip-Gruppe Element', + en: 'Chip Group Element' + }, + // ChipGroupUIElement.chips erwartet BooleanUIElement[], nicht PatternLibraryElement[] + chips: [bool1, bool2] + } as ChipGroupUIElement + }; + } + + case 'StringUIElement': + return { + element: { + pattern_type: 'StringUIElement', + required: false, + type: 'TEXT', + field_id: { + field_name: `string_field_${uuidv4()}` + }, + length_minimum: 0, + length_maximum: 100, + title: { + de: 'String Element', + en: 'String Element' + } + } as StringUIElement + }; + + case 'ImageGalleryUIElement': + return { + element: { + pattern_type: 'ImageGalleryUIElement', + required: false, + preferred_size: 'M', + display_position: 'LEFT', + id_field_value: { field_id: { field_name: '' } }, + title: { + de: 'Bildergalerie', + en: 'Image Gallery' + } + } as ImageGalleryUIElement + }; + + case 'FieldTextUIElement': + return { + element: { + pattern_type: 'FieldTextUIElement', + required: false, + type: 'PARAGRAPH', + field_value: { field_id: { field_name: '' } }, + title: { + de: 'Feldtext', + en: 'Field Text' + } + } as FieldTextUIElement + }; + + case 'TableUIElement': + return { + element: { + pattern_type: 'TableUIElement', + required: false, + type: 'TABLE', + columns: [], + title: { + de: 'Tabelle', + en: 'Table' + } + } as TableUIElement + }; + + case 'ContactUIElement': + return { + element: { + pattern_type: 'ContactUIElement', + required: false, + identity_type: 'USER_ID', + show_name: true, + show_picture: true, + show_details: false, + title: { + de: 'Kontakt', + en: 'Contact' + } + } as ContactUIElement + }; + + default: + // Fallback auf TextUIElement + return { + element: { + pattern_type: 'TextUIElement', + required: false, + type: 'PARAGRAPH', + text: { + de: 'Beispieltext', + en: 'Example text' + }, + title: { + de: 'Text Element', + en: 'Text Element' + } + } as TextUIElement + }; + } +}; diff --git a/src/utils/normalizeUtils.ts b/src/utils/normalizeUtils.ts index 4fcd684..16e6885 100644 --- a/src/utils/normalizeUtils.ts +++ b/src/utils/normalizeUtils.ts @@ -571,11 +571,15 @@ const validateElement = (element: any, path: string): string[] => { errors.push(`${path}: Element hat keine field_id`); } - // Prüfe, ob ein Titel vorhanden ist - if (!element.title) { - errors.push(`${path}: Element hat keinen Titel`); - } else if (typeof element.title === 'object' && (!element.title.de || !element.title.en)) { - errors.push(`${path}: Element hat keinen vollständigen Titel (de/en)`); + // Prüfe, ob ein Titel vorhanden ist. CustomUIElemente (SCANNER/ADDRESS/…) sind + // ausgenommen: portal-applications rendert sie mit eigener Überschrift, das Schema + // kennt für CustomUIElement kein `title` — ein erzwungener Titel wäre Schema-Drift. + if (element.pattern_type !== 'CustomUIElement') { + if (!element.title) { + errors.push(`${path}: Element hat keinen Titel`); + } else if (typeof element.title === 'object' && (!element.title.de || !element.title.en)) { + errors.push(`${path}: Element hat keinen vollständigen Titel (de/en)`); + } } // Prüfe spezifische Elementtypen