- | {scraper.task_name ?? "-"} |
+
+ {scraper.task_name ?? "-"}
+ |
{scraper.frequency ?? "-"}
|
@@ -529,7 +682,7 @@ export function ScrapersTab() {
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-medium",
scraper.enabled
? "bg-emerald-100 text-emerald-700"
- : "bg-gray-200 text-gray-600"
+ : "bg-gray-200 text-gray-600",
)}
>
Actif: {scraper.enabled ? "Oui" : "Non"}
@@ -540,12 +693,19 @@ export function ScrapersTab() {
Strict: {scraper.only_match ? "Oui" : "Non"}
+
+ Une fois: {scraper.once ? "Oui" : "Non"}
+
- Derniere vue: {scraper.last_seen_days ?? "-"}
- Premiere vue: {scraper.first_seen_days ?? "-"}
+
+ Derniere vue: {scraper.last_seen_days ?? "-"}
+
+
+ Premiere vue: {scraper.first_seen_days ?? "-"}
+
Page size: {scraper.page_size ?? "-"}
Max pages: {scraper.max_pages ?? "-"}
@@ -560,13 +720,22 @@ export function ScrapersTab() {
setStatusMessage(null);
setEditingScraperId(scraper.id);
setParamsValid(true);
- let parsedParams: JsonObject = buildDefaultValue(scraperSchema) as JsonObject;
+ let parsedParams: JsonObject = buildDefaultValue(
+ scraperSchema,
+ ) as JsonObject;
if (scraper.params) {
try {
- const raw = JSON.parse(scraper.params) as JsonObject;
- parsedParams = raw && typeof raw === "object" ? raw : parsedParams;
+ const raw = JSON.parse(
+ scraper.params,
+ ) as JsonObject;
+ parsedParams =
+ raw && typeof raw === "object"
+ ? raw
+ : parsedParams;
} catch (error) {
- parsedParams = buildDefaultValue(scraperSchema) as JsonObject;
+ parsedParams = buildDefaultValue(
+ scraperSchema,
+ ) as JsonObject;
}
}
form.reset({
@@ -574,13 +743,16 @@ export function ScrapersTab() {
frequency: scraper.frequency ?? "",
task_name: scraper.task_name ?? "",
property_types: scraper.property_types ?? "",
- last_seen_days: scraper.last_seen_days?.toString() ?? "",
- first_seen_days: scraper.first_seen_days?.toString() ?? "",
+ last_seen_days:
+ scraper.last_seen_days?.toString() ?? "",
+ first_seen_days:
+ scraper.first_seen_days?.toString() ?? "",
page_size: scraper.page_size?.toString() ?? "",
max_pages: scraper.max_pages?.toString() ?? "",
enabled: Boolean(scraper.enabled),
enrich_llm: Boolean(scraper.enrich_llm),
only_match: Boolean(scraper.only_match),
+ once: Boolean(scraper.once),
});
}}
>
@@ -594,7 +766,7 @@ export function ScrapersTab() {
onClick={() => {
if (
window.confirm(
- "Confirmez la suppression de ce scraper ?"
+ "Confirmez la suppression de ce scraper ?",
)
) {
deleteScraperMutation.mutate(scraper.id);
@@ -632,7 +804,9 @@ export function ScrapersTab() {
{countScraperMutation.isPending ? (
- Calcul en cours...
+
+ Calcul en cours...
+
) : countScraperMutation.isError ? (
{getErrorMessage(countScraperMutation.error)}
@@ -640,7 +814,10 @@ export function ScrapersTab() {
) : countScraperMutation.data ? (
Ce scraper correspond a
- {countScraperMutation.data.count}
+
+ {" "}
+ {countScraperMutation.data.count}{" "}
+
annonce(s) potentielle(s).
) : (
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 7fab3a0..f041e0a 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -1,5 +1,7 @@
const DEFAULT_BASE_URL =
- import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
+ import.meta.env.VITE_API_BASE_URL ?? "http://localhost:3000";
+
+const API_TOKEN = import.meta.env.VITE_API_TOKEN ?? "";
const SESSION_TOKEN_STORAGE_KEY = "managinator-session-token";
@@ -52,24 +54,30 @@ async function request (
options?: ApiRequestOptions,
): Promise {
const url = buildUrl(path, options?.query);
+ console.log(`🚀 [API] ${method} ${url}`);
+ console.log(`🔑 [API] Has API_TOKEN: ${!!API_TOKEN}`);
const headers: HeadersInit = {
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
+ ...(API_TOKEN ? { Authorization: `Bearer ${API_TOKEN}` } : {}),
...(sessionToken
? {
"X-Session-Token": sessionToken,
- Authorization: `Bearer ${sessionToken}`,
}
: {}),
...options?.headers,
};
+ console.log(`📤 [API] Headers:`, headers);
+
const response = await fetch(url, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
});
+ console.log(`📥 [API] Response status: ${response.status}`);
+
if (!response.ok) {
let errorPayload: unknown;
const contentType = response.headers.get("content-type") ?? "";
diff --git a/frontend/src/schemas/loader.ts b/frontend/src/schemas/loader.ts
index e236d8e..0492750 100644
--- a/frontend/src/schemas/loader.ts
+++ b/frontend/src/schemas/loader.ts
@@ -2,6 +2,7 @@ import scraperSchemaSource from "../../scraper-schema.json";
import { normalizeRootSchema } from "./normalize";
import { profileSchema } from "./profile-schema";
+import { enrichScraperSchema } from "./scraper-schema-labels";
import type { SchemaDefinition } from "./types";
type SchemaRegistry = {
@@ -10,7 +11,7 @@ type SchemaRegistry = {
};
const registry: SchemaRegistry = {
- scraper: normalizeRootSchema(scraperSchemaSource),
+ scraper: enrichScraperSchema(normalizeRootSchema(scraperSchemaSource)),
profile: profileSchema,
};
diff --git a/frontend/src/schemas/profile-schema.ts b/frontend/src/schemas/profile-schema.ts
index 210d69d..7d79659 100644
--- a/frontend/src/schemas/profile-schema.ts
+++ b/frontend/src/schemas/profile-schema.ts
@@ -41,8 +41,8 @@ const characteristicsSchema: SchemaObjectNode = {
kind: "enum",
options: ["any", "all", "none"],
required: false,
- label: "Evaluation mode",
- description: "Defines how the boolean flags are evaluated.",
+ label: "Mode d'évaluation",
+ description: "Définit comment les caractéristiques sont évaluées (une, toutes, aucune)",
},
},
{
@@ -53,6 +53,7 @@ const characteristicsSchema: SchemaObjectNode = {
type: "string",
required: false,
label: "Description",
+ description: "Description du groupe de caractéristiques",
},
},
{
@@ -66,8 +67,8 @@ const characteristicsSchema: SchemaObjectNode = {
options: characteristicOptions,
required: false,
},
- label: "Feature identifiers",
- description: "List of boolean feature identifiers to evaluate.",
+ label: "Caractéristiques",
+ description: "Liste des caractéristiques à évaluer pour le bien",
},
},
],
@@ -81,51 +82,111 @@ const poiConstraintSchema: SchemaObjectNode = {
key: "poi_category",
required: false,
schema: {
- kind: "primitive",
- type: "string",
+ kind: "enum",
+ options: [
+ { value: "EDUCATION", label: "EDUCATION (écoles, universités, établissements éducatifs)" },
+ { value: "PARKS", label: "PARKS (parcs, jardins, aires de jeux)" },
+ { value: "HEALTH", label: "HEALTH (hôpitaux, médecins, pharmacies, dentistes, vétérinaires)" },
+ { value: "SERVICES", label: "SERVICES (bureaux de poste, banques, blanchisseries, salons de coiffure)" },
+ { value: "SPORT", label: "SPORT (salles de sport, stades, complexes sportifs, piscines)" },
+ { value: "FOOD", label: "FOOD (restaurants, cafés, boulangeries)" },
+ { value: "TRANSPORT", label: "TRANSPORT (gares, stations de métro, stations de bus)" },
+ { value: "SHOPPING", label: "SHOPPING (supermarchés, centres commerciaux, épiceries)" },
+ { value: "SKI_AREA", label: "SKI_AREA (domaines skiables)" },
+ { value: "SKI_LIFT", label: "SKI_LIFT (remontées mécaniques)" },
+ ],
required: false,
- label: "POI category",
+ label: "Catégorie de POI",
+ description: "Type de point d'intérêt recherché à proximité",
},
},
{
key: "max_walk_time_minutes",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Temps de marche max (min)",
+ description: "Temps de marche maximum pour atteindre le POI",
+ },
},
{
key: "max_car_time_minutes",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Temps en voiture max (min)",
+ description: "Temps en voiture maximum pour atteindre le POI",
+ },
},
{
key: "max_transport_time_minutes",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Temps en transport max (min)",
+ description: "Temps en transport en commun maximum pour atteindre le POI",
+ },
},
{
key: "max_bike_time_minutes",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Temps à vélo max (min)",
+ description: "Temps à vélo maximum pour atteindre le POI",
+ },
},
{
key: "max_distance_meters",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Distance maximale (m)",
+ description: "Distance maximale en mètres jusqu'au POI",
+ },
},
{
key: "lift_type",
required: false,
- schema: { kind: "primitive", type: "string", required: false },
+ schema: {
+ kind: "primitive",
+ type: "string",
+ required: false,
+ label: "Type de remontée",
+ description: "Type de remontée mécanique (télésiège, téléphérique, etc.)",
+ },
},
{
key: "domain_min_altitude",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Altitude minimale (m)",
+ description: "Altitude minimale du domaine skiable en mètres",
+ },
},
{
key: "min_rating",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Note minimale",
+ description: "Note minimale requise pour le POI (sur 5)",
+ },
},
],
};
@@ -138,6 +199,8 @@ const visionWeightFields: SchemaObjectNode["fields"] = [
kind: "primitive",
type: "number",
required: false,
+ label: "Poids",
+ description: "Importance de ce critère dans l'évaluation",
},
},
{
@@ -147,6 +210,8 @@ const visionWeightFields: SchemaObjectNode["fields"] = [
kind: "primitive",
type: "number",
required: false,
+ label: "Pénalité si absent",
+ description: "Pénalité appliquée si ce critère est manquant",
},
},
];
@@ -162,44 +227,86 @@ export const profileSchema: SchemaDefinition = {
kind: "enum",
options: transactionOptions,
required: false,
- label: "Transaction type",
- description: "Desired transaction type.",
+ label: "Type de transaction",
+ description: "Type de transaction souhaité (vente, location, viager)",
},
},
{
key: "min_price",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Prix minimum (€)",
+ description: "Prix minimum du bien en euros",
+ },
},
{
key: "max_price",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Prix maximum (€)",
+ description: "Prix maximum du bien en euros",
+ },
},
{
key: "min_size",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Surface minimale (m²)",
+ description: "Surface minimale du bien en mètres carrés",
+ },
},
{
key: "max_size",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Surface maximale (m²)",
+ description: "Surface maximale du bien en mètres carrés",
+ },
},
{
key: "min_bedrooms",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Nombre de chambres min",
+ description: "Nombre minimum de chambres",
+ },
},
{
key: "max_bedrooms",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Nombre de chambres max",
+ description: "Nombre maximum de chambres",
+ },
},
{
key: "min_bathrooms",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Nombre de salles de bain min",
+ description: "Nombre minimum de salles de bain",
+ },
},
{
key: "property_type",
@@ -208,7 +315,8 @@ export const profileSchema: SchemaDefinition = {
kind: "array",
required: false,
element: { kind: "primitive", type: "string", required: false },
- description: "List of property types (apartment, house, etc.).",
+ label: "Types de biens",
+ description: "Types de biens recherchés (appartement, maison, etc.)",
},
},
{
@@ -219,7 +327,8 @@ export const profileSchema: SchemaDefinition = {
options: dpeClassOptions,
multiple: true,
required: false,
- description: "Accepted energy classes.",
+ label: "Classes DPE acceptées",
+ description: "Classes de diagnostic de performance énergétique acceptées",
},
},
{
@@ -230,13 +339,20 @@ export const profileSchema: SchemaDefinition = {
options: dpeClassOptions,
multiple: true,
required: false,
- description: "Accepted greenhouse emission classes.",
+ label: "Classes GES acceptées",
+ description: "Classes d'émissions de gaz à effet de serre acceptées",
},
},
{
key: "city",
required: false,
- schema: { kind: "primitive", type: "string", required: false },
+ schema: {
+ kind: "primitive",
+ type: "string",
+ required: false,
+ label: "Ville",
+ description: "Nom de la ville recherchée",
+ },
},
{
key: "cities",
@@ -245,27 +361,53 @@ export const profileSchema: SchemaDefinition = {
kind: "array",
required: false,
element: { kind: "primitive", type: "string", required: false },
+ label: "Villes",
+ description: "Liste des villes recherchées",
},
},
{
key: "postal_code",
required: false,
- schema: { kind: "primitive", type: "string", required: false },
+ schema: {
+ kind: "primitive",
+ type: "string",
+ required: false,
+ label: "Code postal",
+ description: "Code postal de la zone recherchée",
+ },
},
{
key: "code_insee",
required: false,
- schema: { kind: "primitive", type: "string", required: false },
+ schema: {
+ kind: "primitive",
+ type: "string",
+ required: false,
+ label: "Code INSEE",
+ description: "Code INSEE de la commune",
+ },
},
{
key: "target_yield",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Rendement cible (%)",
+ description: "Rendement locatif cible en pourcentage",
+ },
},
{
key: "require_address",
required: false,
- schema: { kind: "primitive", type: "boolean", required: false },
+ schema: {
+ kind: "primitive",
+ type: "boolean",
+ required: false,
+ label: "Adresse requise",
+ description: "Exiger une adresse complète pour le bien",
+ },
},
{
key: "characteristics",
@@ -274,7 +416,8 @@ export const profileSchema: SchemaDefinition = {
kind: "array",
required: false,
element: characteristicsSchema,
- description: "Boolean feature groups to enforce.",
+ label: "Caractéristiques",
+ description: "Groupes de caractéristiques du bien à rechercher",
},
},
{
@@ -284,7 +427,8 @@ export const profileSchema: SchemaDefinition = {
kind: "array",
required: false,
element: poiConstraintSchema,
- description: "Proximity constraints relative to points of interest.",
+ label: "Proximité POI",
+ description: "Contraintes de proximité par rapport aux points d'intérêt",
},
},
{
@@ -293,11 +437,19 @@ export const profileSchema: SchemaDefinition = {
schema: {
kind: "object",
required: false,
+ label: "Exigences visuelles",
+ description: "Critères d'évaluation basés sur l'analyse visuelle du bien",
fields: [
{
key: "description",
required: false,
- schema: { kind: "primitive", type: "string", required: false },
+ schema: {
+ kind: "primitive",
+ type: "string",
+ required: false,
+ label: "Description",
+ description: "Description générale des exigences visuelles",
+ },
},
{
key: "kitchen",
@@ -305,26 +457,52 @@ export const profileSchema: SchemaDefinition = {
schema: {
kind: "object",
required: false,
+ label: "Cuisine",
+ description: "Exigences pour la cuisine",
fields: [
{
key: "description",
required: false,
- schema: { kind: "primitive", type: "string", required: false },
+ schema: {
+ kind: "primitive",
+ type: "string",
+ required: false,
+ label: "Description",
+ description: "Description des attentes pour la cuisine",
+ },
},
{
key: "size",
required: false,
- schema: { kind: "enum", options: visionSizeOptions, required: false },
+ schema: {
+ kind: "enum",
+ options: visionSizeOptions,
+ required: false,
+ label: "Taille",
+ description: "Taille souhaitée de la cuisine (Small, Medium, Large)",
+ },
},
{
key: "open",
required: false,
- schema: { kind: "primitive", type: "boolean", required: false },
+ schema: {
+ kind: "primitive",
+ type: "boolean",
+ required: false,
+ label: "Cuisine ouverte",
+ description: "La cuisine doit être ouverte sur le salon",
+ },
},
{
key: "appliances",
required: false,
- schema: { kind: "primitive", type: "string", required: false },
+ schema: {
+ kind: "primitive",
+ type: "string",
+ required: false,
+ label: "Équipements",
+ description: "Liste des équipements souhaités dans la cuisine",
+ },
},
...visionWeightFields,
],
@@ -336,21 +514,41 @@ export const profileSchema: SchemaDefinition = {
schema: {
kind: "object",
required: false,
+ label: "Salle de bain",
+ description: "Exigences pour la salle de bain",
fields: [
{
key: "description",
required: false,
- schema: { kind: "primitive", type: "string", required: false },
+ schema: {
+ kind: "primitive",
+ type: "string",
+ required: false,
+ label: "Description",
+ description: "Description des attentes pour la salle de bain",
+ },
},
{
key: "size",
required: false,
- schema: { kind: "enum", options: visionSizeOptions, required: false },
+ schema: {
+ kind: "enum",
+ options: visionSizeOptions,
+ required: false,
+ label: "Taille",
+ description: "Taille souhaitée de la salle de bain",
+ },
},
{
key: "appliances",
required: false,
- schema: { kind: "primitive", type: "string", required: false },
+ schema: {
+ kind: "primitive",
+ type: "string",
+ required: false,
+ label: "Équipements",
+ description: "Liste des équipements souhaités (baignoire, douche, etc.)",
+ },
},
...visionWeightFields,
],
@@ -362,16 +560,30 @@ export const profileSchema: SchemaDefinition = {
schema: {
kind: "object",
required: false,
+ label: "Salon",
+ description: "Exigences pour le salon",
fields: [
{
key: "description",
required: false,
- schema: { kind: "primitive", type: "string", required: false },
+ schema: {
+ kind: "primitive",
+ type: "string",
+ required: false,
+ label: "Description",
+ description: "Description des attentes pour le salon",
+ },
},
{
key: "size",
required: false,
- schema: { kind: "enum", options: visionSizeOptions, required: false },
+ schema: {
+ kind: "enum",
+ options: visionSizeOptions,
+ required: false,
+ label: "Taille",
+ description: "Taille souhaitée du salon",
+ },
},
...visionWeightFields,
],
@@ -383,16 +595,30 @@ export const profileSchema: SchemaDefinition = {
schema: {
kind: "object",
required: false,
+ label: "Chambres",
+ description: "Exigences pour les chambres",
fields: [
{
key: "description",
required: false,
- schema: { kind: "primitive", type: "string", required: false },
+ schema: {
+ kind: "primitive",
+ type: "string",
+ required: false,
+ label: "Description",
+ description: "Description des attentes pour les chambres",
+ },
},
{
key: "min_count",
required: false,
- schema: { kind: "primitive", type: "number", required: false },
+ schema: {
+ kind: "primitive",
+ type: "number",
+ required: false,
+ label: "Nombre minimum",
+ description: "Nombre minimum de chambres requis",
+ },
},
...visionWeightFields,
],
@@ -404,16 +630,30 @@ export const profileSchema: SchemaDefinition = {
schema: {
kind: "object",
required: false,
+ label: "Espaces extérieurs",
+ description: "Exigences pour les espaces extérieurs",
fields: [
{
key: "description",
required: false,
- schema: { kind: "primitive", type: "string", required: false },
+ schema: {
+ kind: "primitive",
+ type: "string",
+ required: false,
+ label: "Description",
+ description: "Description des attentes pour les espaces extérieurs",
+ },
},
{
key: "required",
required: false,
- schema: { kind: "primitive", type: "boolean", required: false },
+ schema: {
+ kind: "primitive",
+ type: "boolean",
+ required: false,
+ label: "Requis",
+ description: "Un espace extérieur est-il obligatoire",
+ },
},
{
key: "types",
@@ -422,6 +662,8 @@ export const profileSchema: SchemaDefinition = {
kind: "array",
required: false,
element: { kind: "primitive", type: "string", required: false },
+ label: "Types d'espaces",
+ description: "Types d'espaces extérieurs souhaités (terrasse, jardin, balcon, etc.)",
},
},
...visionWeightFields,
@@ -431,7 +673,13 @@ export const profileSchema: SchemaDefinition = {
{
key: "note",
required: false,
- schema: { kind: "primitive", type: "string", required: false },
+ schema: {
+ kind: "primitive",
+ type: "string",
+ required: false,
+ label: "Note",
+ description: "Remarques ou notes supplémentaires sur les exigences visuelles",
+ },
},
],
},
diff --git a/frontend/src/schemas/scraper-schema-labels.ts b/frontend/src/schemas/scraper-schema-labels.ts
new file mode 100644
index 0000000..9214841
--- /dev/null
+++ b/frontend/src/schemas/scraper-schema-labels.ts
@@ -0,0 +1,571 @@
+/**
+ * Métadonnées (labels et descriptions) pour le schéma scraper.
+ * Ces informations sont injectées dans le schéma après normalisation.
+ */
+
+import type { SchemaDefinition, SchemaNode, SchemaObjectNode } from "./types";
+
+export interface FieldMetadata {
+ label?: string;
+ description?: string;
+}
+
+export const scraperFieldLabels: Record = {
+ // Métadonnées globales
+ "meta": {
+ label: "Métadonnées",
+ description: "Informations temporelles sur les annonces",
+ },
+ "meta.firstSeenAt": {
+ label: "Première apparition",
+ description: "Période de première apparition de l'annonce",
+ },
+ "meta.firstSeenAt.min": {
+ label: "Date min",
+ description: "Date minimale de première apparition",
+ },
+ "meta.firstSeenAt.max": {
+ label: "Date max",
+ description: "Date maximale de première apparition",
+ },
+ "meta.lastSeenAt": {
+ label: "Dernière vue",
+ description: "Période de dernière visibilité de l'annonce",
+ },
+ "meta.lastSeenAt.min": {
+ label: "Date min",
+ description: "Date minimale de dernière vue",
+ },
+ "meta.lastSeenAt.max": {
+ label: "Date max",
+ description: "Date maximale de dernière vue",
+ },
+ "meta.lastPublishedAt": {
+ label: "Dernière publication",
+ description: "Période de dernière publication",
+ },
+ "meta.lastUpdatedAt": {
+ label: "Dernière mise à jour",
+ description: "Période de dernière mise à jour",
+ },
+ "meta.isTotallyOffline": {
+ label: "Totalement hors ligne",
+ description: "Annonce complètement retirée de toutes les plateformes",
+ },
+
+ // Prix
+ "price": {
+ label: "Prix",
+ description: "Critères de prix du bien",
+ },
+ "price.currency": {
+ label: "Devise",
+ description: "Devise du prix",
+ },
+ "price.isAuction": {
+ label: "Est aux enchères",
+ description: "Le bien est vendu aux enchères",
+ },
+ "price.scope": {
+ label: "Périodicité",
+ description: "Paiement unique ou mensuel (pour locations)",
+ },
+ "price.latest": {
+ label: "Prix actuel",
+ description: "Prix le plus récent",
+ },
+ "price.latest.value": {
+ label: "Valeur",
+ description: "Fourchette de prix",
+ },
+ "price.latest.value.min": {
+ label: "Prix min (€)",
+ description: "Prix minimum",
+ },
+ "price.latest.value.max": {
+ label: "Prix max (€)",
+ description: "Prix maximum",
+ },
+ "price.latest.valuePerArea": {
+ label: "Prix au m²",
+ description: "Prix par mètre carré",
+ },
+ "price.latest.valuePerArea.min": {
+ label: "Prix/m² min (€)",
+ description: "Prix minimum par m²",
+ },
+ "price.latest.valuePerArea.max": {
+ label: "Prix/m² max (€)",
+ description: "Prix maximum par m²",
+ },
+ "price.initial": {
+ label: "Prix initial",
+ description: "Prix de départ",
+ },
+ "price.initial.value": {
+ label: "Valeur initiale",
+ description: "Fourchette de prix initial",
+ },
+ "price.warrantyDeposit": {
+ label: "Dépôt de garantie",
+ description: "Montant du dépôt de garantie (pour locations)",
+ },
+ "price.warrantyDeposit.min": {
+ label: "Min (€)",
+ description: "Dépôt minimum",
+ },
+ "price.warrantyDeposit.max": {
+ label: "Max (€)",
+ description: "Dépôt maximum",
+ },
+
+ // Localisation
+ "location": {
+ label: "Localisation",
+ description: "Critères de localisation géographique",
+ },
+ "location.city": {
+ label: "Ville",
+ description: "Nom de la ville",
+ },
+ "location.postalCode": {
+ label: "Code postal",
+ description: "Code postal de la zone",
+ },
+ "location.department": {
+ label: "Département",
+ description: "Nom du département",
+ },
+ "location.inseeCode": {
+ label: "Code INSEE",
+ description: "Code INSEE de la commune",
+ },
+ "location.irisCode": {
+ label: "Code IRIS",
+ description: "Code IRIS (quartier)",
+ },
+ "location.locationCoordinate": {
+ label: "Coordonnées GPS",
+ description: "Position géographique exacte",
+ },
+ "location.locationCoordinate.location.lat": {
+ label: "Latitude",
+ description: "Latitude GPS",
+ },
+ "location.locationCoordinate.location.lon": {
+ label: "Longitude",
+ description: "Longitude GPS",
+ },
+
+ // Habitation
+ "habitation": {
+ label: "Habitation",
+ description: "Caractéristiques de l'habitation",
+ },
+ "habitation.type": {
+ label: "Type de bien",
+ description: "Type de propriété (appartement, maison, etc.)",
+ },
+ "habitation.roomCount": {
+ label: "Nombre de pièces",
+ description: "Nombre total de pièces",
+ },
+ "habitation.roomCount.min": {
+ label: "Min",
+ description: "Nombre minimum de pièces",
+ },
+ "habitation.roomCount.max": {
+ label: "Max",
+ description: "Nombre maximum de pièces",
+ },
+ "habitation.bedroomCount": {
+ label: "Nombre de chambres",
+ description: "Nombre de chambres",
+ },
+ "habitation.bedroomCount.min": {
+ label: "Min",
+ description: "Nombre minimum de chambres",
+ },
+ "habitation.bedroomCount.max": {
+ label: "Max",
+ description: "Nombre maximum de chambres",
+ },
+ "habitation.bathroomCount": {
+ label: "Salles de bain",
+ description: "Nombre de salles de bain",
+ },
+ "habitation.bathroomCount.min": {
+ label: "Min",
+ description: "Minimum de salles de bain",
+ },
+ "habitation.bathroomCount.max": {
+ label: "Max",
+ description: "Maximum de salles de bain",
+ },
+ "habitation.wcCount": {
+ label: "Toilettes",
+ description: "Nombre de WC",
+ },
+
+ // Surface
+ "habitation.surface": {
+ label: "Surfaces",
+ description: "Surfaces du bien",
+ },
+ "habitation.surface.total": {
+ label: "Surface totale",
+ description: "Surface totale du bien",
+ },
+ "habitation.surface.total.min": {
+ label: "Min (m²)",
+ description: "Surface minimale",
+ },
+ "habitation.surface.total.max": {
+ label: "Max (m²)",
+ description: "Surface maximale",
+ },
+ "habitation.surface.livingSpace": {
+ label: "Surface habitable",
+ description: "Surface habitable",
+ },
+ "habitation.surface.livingSpace.min": {
+ label: "Min (m²)",
+ description: "Surface habitable minimale",
+ },
+ "habitation.surface.livingSpace.max": {
+ label: "Max (m²)",
+ description: "Surface habitable maximale",
+ },
+ "habitation.surface.livingroom": {
+ label: "Surface salon",
+ description: "Surface du salon",
+ },
+ "habitation.surface.kitchen": {
+ label: "Surface cuisine",
+ description: "Surface de la cuisine",
+ },
+ "habitation.surface.terraces": {
+ label: "Surface terrasses",
+ description: "Surface des terrasses",
+ },
+ "habitation.surface.balconies": {
+ label: "Surface balcons",
+ description: "Surface des balcons",
+ },
+ "habitation.surface.gardens": {
+ label: "Surface jardins",
+ description: "Surface des jardins",
+ },
+
+ // Caractéristiques
+ "habitation.characteristics": {
+ label: "Caractéristiques",
+ description: "Caractéristiques et équipements",
+ },
+ "habitation.characteristics.hasBalcony": {
+ label: "Balcon",
+ description: "Présence d'un balcon",
+ },
+ "habitation.characteristics.hasTerrace": {
+ label: "Terrasse",
+ description: "Présence d'une terrasse",
+ },
+ "habitation.characteristics.hasGarden": {
+ label: "Jardin",
+ description: "Présence d'un jardin",
+ },
+ "habitation.characteristics.hasParking": {
+ label: "Parking",
+ description: "Place de parking disponible",
+ },
+ "habitation.characteristics.hasGarage": {
+ label: "Garage",
+ description: "Garage disponible",
+ },
+ "habitation.characteristics.hasPool": {
+ label: "Piscine",
+ description: "Présence d'une piscine",
+ },
+ "habitation.characteristics.hasLift": {
+ label: "Ascenseur",
+ description: "Immeuble avec ascenseur",
+ },
+ "habitation.characteristics.hasCellar": {
+ label: "Cave",
+ description: "Cave disponible",
+ },
+ "habitation.characteristics.hasFireplace": {
+ label: "Cheminée",
+ description: "Présence d'une cheminée",
+ },
+ "habitation.characteristics.hasAlarm": {
+ label: "Alarme",
+ description: "Système d'alarme installé",
+ },
+
+ // DPE / Climat
+ "habitation.climate": {
+ label: "Performance énergétique",
+ description: "Diagnostic de performance énergétique",
+ },
+ "habitation.climate.epcEnergy": {
+ label: "Classe DPE",
+ description: "Classe de diagnostic de performance énergétique",
+ },
+ "habitation.climate.epcClimate": {
+ label: "Classe GES",
+ description: "Classe d'émissions de gaz à effet de serre",
+ },
+ "habitation.climate.epcEnergyScore": {
+ label: "Score DPE",
+ description: "Score numérique DPE",
+ },
+ "habitation.climate.epcClimateScore": {
+ label: "Score GES",
+ description: "Score numérique GES",
+ },
+
+ // État du bien
+ "habitation.propertyCondition": {
+ label: "État du bien",
+ description: "Condition et année de construction",
+ },
+ "habitation.propertyCondition.constructionYear": {
+ label: "Année de construction",
+ description: "Année de construction du bien",
+ },
+ "habitation.propertyCondition.renovationYear": {
+ label: "Année de rénovation",
+ description: "Année de dernière rénovation",
+ },
+ "habitation.propertyCondition.interiorCondition": {
+ label: "État intérieur",
+ description: "État de l'intérieur du bien",
+ },
+
+ // Étage
+ "habitation.features": {
+ label: "Caractéristiques",
+ description: "Caractéristiques détaillées",
+ },
+ "habitation.features.propertyFloor": {
+ label: "Étage",
+ description: "Étage du bien",
+ },
+ "habitation.features.propertyFloor.min": {
+ label: "Étage min",
+ description: "Étage minimum",
+ },
+ "habitation.features.propertyFloor.max": {
+ label: "Étage max",
+ description: "Étage maximum",
+ },
+ "habitation.features.propertyTotalFloor": {
+ label: "Étages total",
+ description: "Nombre total d'étages de l'immeuble",
+ },
+ "habitation.features.exposure": {
+ label: "Exposition",
+ description: "Exposition du bien (nord, sud, etc.)",
+ },
+ "habitation.features.furniture": {
+ label: "Ameublement",
+ description: "État d'ameublement du bien",
+ },
+
+ // Chauffage
+ "habitation.heatings": {
+ label: "Type de chauffage",
+ description: "Source d'énergie pour le chauffage",
+ },
+ "habitation.heatTypes": {
+ label: "Système de chauffage",
+ description: "Type de système de chauffage",
+ },
+ "habitation.heatTypeDetails": {
+ label: "Détails chauffage",
+ description: "Détails du système de chauffage",
+ },
+
+ // Terrain
+ "land": {
+ label: "Terrain",
+ description: "Caractéristiques du terrain",
+ },
+ "land.surface": {
+ label: "Surface terrain",
+ description: "Surface du terrain",
+ },
+ "land.surface.min": {
+ label: "Min (m²)",
+ description: "Surface minimale du terrain",
+ },
+ "land.surface.max": {
+ label: "Max (m²)",
+ description: "Surface maximale du terrain",
+ },
+ "land.type": {
+ label: "Type de terrain",
+ description: "Type de terrain",
+ },
+ "land.canConstruct": {
+ label: "Constructible",
+ description: "Terrain constructible",
+ },
+ "land.isServiced": {
+ label: "Viabilisé",
+ description: "Terrain viabilisé",
+ },
+ "land.haveBuildingPermit": {
+ label: "Permis de construire",
+ description: "Permis de construire obtenu",
+ },
+
+ // Type de transaction
+ "type": {
+ label: "Type de bien",
+ description: "Catégorie de bien immobilier",
+ },
+ "offer": {
+ label: "Offre",
+ description: "Type d'offre immobilière",
+ },
+ "offer.type": {
+ label: "Type de transaction",
+ description: "Achat, location, viager, etc.",
+ },
+ "offer.isCurrentlyOccupied": {
+ label: "Actuellement occupé",
+ description: "Le bien est actuellement occupé",
+ },
+ "offer.renting": {
+ label: "Location",
+ description: "Détails de la location",
+ },
+ "offer.renting.isColocation": {
+ label: "Colocation",
+ description: "Location en colocation",
+ },
+ "offer.renting.isLongTerm": {
+ label: "Longue durée",
+ description: "Location longue durée",
+ },
+ "offer.renting.isShortTerm": {
+ label: "Courte durée",
+ description: "Location courte durée / saisonnière",
+ },
+
+ // Parking
+ "parking": {
+ label: "Parking",
+ description: "Informations sur le parking",
+ },
+ "parking.count": {
+ label: "Nombre de places",
+ description: "Nombre de places de parking",
+ },
+ "parking.count.min": {
+ label: "Min",
+ description: "Nombre minimum de places",
+ },
+ "parking.count.max": {
+ label: "Max",
+ description: "Nombre maximum de places",
+ },
+ "parking.type": {
+ label: "Type",
+ description: "Type de parking (garage, extérieur)",
+ },
+ "parking.surface": {
+ label: "Surface",
+ description: "Surface du parking",
+ },
+
+ // Annonces
+ "adverts": {
+ label: "Annonces",
+ description: "Critères sur les annonces elles-mêmes",
+ },
+ "adverts.isPro": {
+ label: "Annonce professionnelle",
+ description: "Annonce d'un professionnel",
+ },
+ "adverts.isOnline": {
+ label: "En ligne",
+ description: "Annonce actuellement visible",
+ },
+ "adverts.hasAnomaly": {
+ label: "A des anomalies",
+ description: "Présence d'anomalies détectées",
+ },
+ "adverts.isExclusive": {
+ label: "Exclusivité",
+ description: "Annonce en exclusivité",
+ },
+
+ // Global
+ "process": {
+ label: "Statut",
+ description: "Statut de l'annonce dans le processus de vente",
+ },
+ "isUrgent": {
+ label: "Urgent",
+ description: "Annonce marquée comme urgente",
+ },
+ "hasAnomaly": {
+ label: "A des anomalies",
+ description: "Des anomalies ont été détectées",
+ },
+ "tags": {
+ label: "Tags",
+ description: "Tags personnalisés",
+ },
+};
+
+/**
+ * Enrichit récursivement un schéma avec les labels et descriptions.
+ */
+function enrichSchemaNode(node: SchemaNode, path: string): SchemaNode {
+ const metadata = scraperFieldLabels[path];
+ const enrichedNode = { ...node };
+
+ if (metadata) {
+ if (metadata.label) {
+ enrichedNode.label = metadata.label;
+ }
+ if (metadata.description) {
+ enrichedNode.description = metadata.description;
+ }
+ }
+
+ if (enrichedNode.kind === "object") {
+ const objectNode = enrichedNode as SchemaObjectNode;
+ objectNode.fields = objectNode.fields.map((field) => {
+ const fieldPath = path ? `${path}.${field.key}` : field.key;
+ return {
+ ...field,
+ schema: enrichSchemaNode(field.schema, fieldPath),
+ };
+ });
+ } else if (enrichedNode.kind === "array") {
+ // Pour les arrays, on enrichit l'élément mais on garde le même path
+ // car les éléments du array partagent le même schéma
+ enrichedNode.element = enrichSchemaNode(enrichedNode.element, path);
+ } else if (enrichedNode.kind === "range") {
+ // Pour les ranges, on enrichit aussi les min/max si ils existent
+ const minPath = `${path}.min`;
+ const maxPath = `${path}.max`;
+ if (scraperFieldLabels[minPath] || scraperFieldLabels[maxPath]) {
+ // Les ranges n'ont pas de sous-champs mais on peut enrichir le range lui-même
+ }
+ }
+
+ return enrichedNode;
+}
+
+/**
+ * Enrichit un schéma racine avec les labels et descriptions.
+ */
+export function enrichScraperSchema(schema: SchemaDefinition): SchemaDefinition {
+ return enrichSchemaNode(schema, "") as SchemaDefinition;
+}
diff --git a/frontend/src/schemas/types.ts b/frontend/src/schemas/types.ts
index 213371d..238451e 100644
--- a/frontend/src/schemas/types.ts
+++ b/frontend/src/schemas/types.ts
@@ -12,9 +12,11 @@ export interface SchemaPrimitiveNode extends SchemaBaseNode {
type: PrimitiveType;
}
+export type EnumOption = string | { value: string; label: string };
+
export interface SchemaEnumNode extends SchemaBaseNode {
kind: "enum";
- options: string[];
+ options: EnumOption[];
multiple?: boolean;
}
|