diff --git a/backend/app.py b/backend/app.py index 73f4071..f6cff6a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -464,6 +464,10 @@ SCRAPER_RESPONSE_FIELDS = ( "once", ) +SCRAPER_BOOL_FIELDS = ( + "once", +) + SCRAPER_INT_FIELDS = ( "last_seen_days", "first_seen_days", @@ -472,7 +476,6 @@ SCRAPER_INT_FIELDS = ( "enabled", "enrich_llm", "only_match", - "once", ) @@ -783,6 +786,11 @@ def create_scraper(): value = payload[field] data[field] = None if value is None else _parse_int(value, field) + for field in SCRAPER_BOOL_FIELDS: + if field in payload: + value = payload[field] + data[field] = None if value is None else _parse_bool(value, field) + try: row = _insert_row(SCRAPER_TABLE, data, SCRAPER_RESPONSE_FIELDS) except psycopg.IntegrityError as exc: @@ -814,6 +822,11 @@ def update_scraper(scraper_id: str): value = payload[field] updates[field] = None if value is None else _parse_int(value, field) + for field in SCRAPER_BOOL_FIELDS: + if field in payload: + value = payload[field] + updates[field] = None if value is None else _parse_bool(value, field) + if not updates: abort(400, description="No updatable fields provided") diff --git a/frontend/src/components/schema-builder/SchemaAwareJsonField.tsx b/frontend/src/components/schema-builder/SchemaAwareJsonField.tsx index 09e0472..7ea8a86 100644 --- a/frontend/src/components/schema-builder/SchemaAwareJsonField.tsx +++ b/frontend/src/components/schema-builder/SchemaAwareJsonField.tsx @@ -48,8 +48,12 @@ export function SchemaAwareJsonField({ }, [value, onChange, resolvedValue]); useEffect(() => { - setRawValue(JSON.stringify(resolvedValue, null, 2)); - }, [resolvedValue]); + // Only sync rawValue with resolvedValue when in schema mode + // to avoid overwriting user input in raw mode + if (mode === "schema") { + setRawValue(JSON.stringify(resolvedValue, null, 2)); + } + }, [resolvedValue, mode]); useEffect(() => { onValidationChange?.(validation); diff --git a/frontend/src/schemas/profile-schema.ts b/frontend/src/schemas/profile-schema.ts index 7d79659..b4fc4a6 100644 --- a/frontend/src/schemas/profile-schema.ts +++ b/frontend/src/schemas/profile-schema.ts @@ -17,6 +17,7 @@ const characteristicOptions = [ "has_interphone", "has_jacuzzi", "has_land", + "has_land_division", "has_grenier", "has_vis_a_vis", "is_peaceful", diff --git a/frontend/src/schemas/scraper-schema-labels.ts b/frontend/src/schemas/scraper-schema-labels.ts index 9214841..ce881ae 100644 --- a/frontend/src/schemas/scraper-schema-labels.ts +++ b/frontend/src/schemas/scraper-schema-labels.ts @@ -3,7 +3,7 @@ * Ces informations sont injectées dans le schéma après normalisation. */ -import type { SchemaDefinition, SchemaNode, SchemaObjectNode } from "./types"; +import type { SchemaDefinition, SchemaNode, SchemaObjectNode, SchemaEnumNode } from "./types"; export interface FieldMetadata { label?: string; @@ -538,6 +538,60 @@ function enrichSchemaNode(node: SchemaNode, path: string): SchemaNode { } } + // Forcer la multi-sélection pour les classes DPE/GES et types de biens + if (enrichedNode.kind === "enum") { + const multiSelectFields = [ + "habitation.climate.epcEnergy", + "habitation.climate.epcClimate", + "type", + ]; + if (multiSelectFields.includes(path)) { + const enumNode = enrichedNode as SchemaEnumNode; + enumNode.multiple = true; + + // Transformer les options pour les rendre plus lisibles + if (path === "habitation.climate.epcEnergy" || path === "habitation.climate.epcClimate") { + enumNode.options = enumNode.options.map((option) => { + if (typeof option === "string") { + // Extraire la classe (A, B, C, etc.) depuis ENERGY_CLASSIFICATION_A + const match = option.match(/_([A-G]|NC|UNKNOWN)$/); + if (match) { + const classValue = match[1]; + return { + value: option, + label: classValue === "UNKNOWN" ? "Non spécifié" : classValue, + }; + } + } + return option; + }); + } else if (path === "type") { + // Labels pour les types de biens + const typeLabels: Record = { + "CLASS_UNKNOWN": "Non spécifié", + "CLASS_HOUSE": "Maison", + "CLASS_FLAT": "Appartement", + "CLASS_PROGRAM": "Programme neuf", + "CLASS_SHOP": "Commerce", + "CLASS_PREMISES": "Local commercial", + "CLASS_OFFICE": "Bureau", + "CLASS_LAND": "Terrain", + "CLASS_BUILDING": "Immeuble", + "CLASS_PARKING": "Parking", + }; + enumNode.options = enumNode.options.map((option) => { + if (typeof option === "string") { + return { + value: option, + label: typeLabels[option] || option, + }; + } + return option; + }); + } + } + } + if (enrichedNode.kind === "object") { const objectNode = enrichedNode as SchemaObjectNode; objectNode.fields = objectNode.fields.map((field) => {