better ui
This commit is contained in:
parent
ef417be648
commit
c90da606a3
10
backend/.env
10
backend/.env
@ -1,7 +1,7 @@
|
||||
DB_NAME=immonator_test
|
||||
DB_PORT=5433
|
||||
DB_HOST=34.135.132.141
|
||||
DB_USERNAME=django
|
||||
DB_PASSWORD=T17HE59a7Xlu2lBgPUQ9rdCPTe4159UNedGhcOvrTu8=
|
||||
DB_PORT=5432
|
||||
DB_HOST=34.155.247.19
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=3cd70e73f0b73a2411c595f3a2f8cf125ee3937498f82628906ed946e8499a00f7733c6ec588cdff4aec386de3c3cc51d0a166369c6a99e73b9c95d2a1bd115a
|
||||
API_TOKEN=B0uKZQN+qIsLc0yNR/t9xCOkgP6Keg0oarLUiZkO2Mo=
|
||||
|
||||
FLUXIMMO_API_KEY=a
|
||||
|
||||
@ -228,6 +228,26 @@ def _load_scraper_params(value: Any) -> Dict[str, Any]:
|
||||
abort(400, description="Field 'params' must be a JSON object or string")
|
||||
|
||||
|
||||
def _validate_property_types(value: str | None) -> str | None:
|
||||
"""Valide que les types de propriétés sont dans la liste autorisée."""
|
||||
if value is None or value.strip() == "":
|
||||
return None
|
||||
|
||||
valid_types = {"immeuble", "appartement", "maison"}
|
||||
types = [t.strip().lower() for t in value.split(",") if t.strip()]
|
||||
|
||||
invalid_types = [t for t in types if t not in valid_types]
|
||||
|
||||
if invalid_types:
|
||||
abort(
|
||||
400,
|
||||
description=f"Invalid property types: {', '.join(invalid_types)}. "
|
||||
f"Allowed values: {', '.join(sorted(valid_types))}"
|
||||
)
|
||||
|
||||
return value.strip()
|
||||
|
||||
|
||||
def build_scraper_params(
|
||||
params: Dict[str, Any],
|
||||
first_seen_days: int | None,
|
||||
@ -441,6 +461,7 @@ SCRAPER_RESPONSE_FIELDS = (
|
||||
"max_pages",
|
||||
"enrich_llm",
|
||||
"only_match",
|
||||
"once",
|
||||
)
|
||||
|
||||
SCRAPER_INT_FIELDS = (
|
||||
@ -451,6 +472,7 @@ SCRAPER_INT_FIELDS = (
|
||||
"enabled",
|
||||
"enrich_llm",
|
||||
"only_match",
|
||||
"once",
|
||||
)
|
||||
|
||||
|
||||
@ -743,13 +765,19 @@ def create_scraper():
|
||||
|
||||
data: Dict[str, Any] = {"id": scraper_id}
|
||||
|
||||
for field in ("params", "frequency", "task_name", "property_types"):
|
||||
for field in ("params", "frequency", "task_name"):
|
||||
if field in payload:
|
||||
value = payload[field]
|
||||
data[field] = (
|
||||
None if value is None else _parse_string(value, field, allow_empty=True)
|
||||
)
|
||||
|
||||
# Validation spéciale pour property_types
|
||||
if "property_types" in payload:
|
||||
value = payload["property_types"]
|
||||
parsed_value = None if value is None else _parse_string(value, "property_types", allow_empty=True)
|
||||
data["property_types"] = _validate_property_types(parsed_value)
|
||||
|
||||
for field in SCRAPER_INT_FIELDS:
|
||||
if field in payload:
|
||||
value = payload[field]
|
||||
@ -768,13 +796,19 @@ def update_scraper(scraper_id: str):
|
||||
payload = _get_json_body()
|
||||
updates: Dict[str, Any] = {}
|
||||
|
||||
for field in ("params", "frequency", "task_name", "property_types"):
|
||||
for field in ("params", "frequency", "task_name"):
|
||||
if field in payload:
|
||||
value = payload[field]
|
||||
updates[field] = (
|
||||
None if value is None else _parse_string(value, field, allow_empty=True)
|
||||
)
|
||||
|
||||
# Validation spéciale pour property_types
|
||||
if "property_types" in payload:
|
||||
value = payload["property_types"]
|
||||
parsed_value = None if value is None else _parse_string(value, "property_types", allow_empty=True)
|
||||
updates["property_types"] = _validate_property_types(parsed_value)
|
||||
|
||||
for field in SCRAPER_INT_FIELDS:
|
||||
if field in payload:
|
||||
value = payload[field]
|
||||
@ -860,4 +894,4 @@ def count_scraper_properties():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=int(os.getenv("PORT", "8000")), debug=False)
|
||||
app.run(host="0.0.0.0", port=int(os.getenv("PORT", "3000")), debug=False)
|
||||
|
||||
2
frontend/.env
Normal file
2
frontend/.env
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
VITE_API_TOKEN=B0uKZQN+qIsLc0yNR/t9xCOkgP6Keg0oarLUiZkO2Mo=
|
||||
@ -112,6 +112,15 @@ function renderEnum(
|
||||
) {
|
||||
const error = getFirstError(errorMap, path);
|
||||
|
||||
// Helper function to get value and label from enum option
|
||||
const getOptionValue = (option: string | { value: string; label: string }) => {
|
||||
return typeof option === "string" ? option : option.value;
|
||||
};
|
||||
|
||||
const getOptionLabel = (option: string | { value: string; label: string }) => {
|
||||
return typeof option === "string" ? option : option.label;
|
||||
};
|
||||
|
||||
if (schema.multiple) {
|
||||
const current = Array.isArray(value) ? (value as string[]) : [];
|
||||
|
||||
@ -120,20 +129,22 @@ function renderEnum(
|
||||
<SchemaFieldLabel description={schema.description}>{schema.label ?? getLabelFromPath(path)}</SchemaFieldLabel>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{schema.options.map((option) => {
|
||||
const checked = current.includes(option);
|
||||
const optionValue = getOptionValue(option);
|
||||
const optionLabel = getOptionLabel(option);
|
||||
const checked = current.includes(optionValue);
|
||||
return (
|
||||
<label key={option} className="flex items-center gap-2 text-sm">
|
||||
<label key={optionValue} className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onCheckedChange={(next) => {
|
||||
if (next) {
|
||||
onChange([...current, option]);
|
||||
onChange([...current, optionValue]);
|
||||
} else {
|
||||
onChange(current.filter((item) => item !== option));
|
||||
onChange(current.filter((item) => item !== optionValue));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{option}
|
||||
{optionLabel}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
@ -154,11 +165,15 @@ function renderEnum(
|
||||
<SelectValue placeholder="Selectionner" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{schema.options.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
{schema.options.map((option) => {
|
||||
const optionValue = getOptionValue(option);
|
||||
const optionLabel = getOptionLabel(option);
|
||||
return (
|
||||
<SelectItem key={optionValue} value={optionValue}>
|
||||
{optionLabel}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{error ? <FieldError message={error.message} /> : null}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import type {
|
||||
EnumOption,
|
||||
SchemaArrayNode,
|
||||
SchemaEnumNode,
|
||||
SchemaNode,
|
||||
@ -98,6 +99,13 @@ function validateEnum(schema: SchemaEnumNode, value: unknown, path: string) {
|
||||
return schema.required === false ? [] : [createError(path, "Valeur requise.")];
|
||||
}
|
||||
|
||||
// Helper to extract values from options (handles both string and {value, label} formats)
|
||||
const getOptionValue = (option: EnumOption): string => {
|
||||
return typeof option === "string" ? option : option.value;
|
||||
};
|
||||
|
||||
const validValues = schema.options.map(getOptionValue);
|
||||
|
||||
if (schema.multiple) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [createError(path, "Selection multiple attendue.")];
|
||||
@ -107,7 +115,7 @@ function validateEnum(schema: SchemaEnumNode, value: unknown, path: string) {
|
||||
return schema.required === false ? [] : [createError(path, "Valeur requise.")];
|
||||
}
|
||||
|
||||
const invalid = value.filter((item) => !schema.options.includes(item as string));
|
||||
const invalid = value.filter((item) => !validValues.includes(item as string));
|
||||
|
||||
if (invalid.length > 0) {
|
||||
return [createError(path, "Valeurs invalides selectionnees.")];
|
||||
@ -122,7 +130,7 @@ function validateEnum(schema: SchemaEnumNode, value: unknown, path: string) {
|
||||
: [createError(path, "Valeur requise.")];
|
||||
}
|
||||
|
||||
return schema.options.includes(value as string)
|
||||
return validValues.includes(value as string)
|
||||
? []
|
||||
: [createError(path, "Valeur non autorisee.")];
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ export interface Scraper {
|
||||
enabled: number | null;
|
||||
enrich_llm: number | null;
|
||||
only_match: number | null;
|
||||
once: number | null;
|
||||
}
|
||||
|
||||
interface ScraperFormValues {
|
||||
@ -55,6 +56,7 @@ interface ScraperFormValues {
|
||||
enabled: boolean;
|
||||
enrich_llm: boolean;
|
||||
only_match: boolean;
|
||||
once: boolean;
|
||||
}
|
||||
|
||||
interface CreateScraperPayload {
|
||||
@ -70,6 +72,7 @@ interface CreateScraperPayload {
|
||||
enabled?: number | null;
|
||||
enrich_llm?: number | null;
|
||||
only_match?: number | null;
|
||||
once?: number | null;
|
||||
}
|
||||
|
||||
const scraperSchema = getSchema("scraper");
|
||||
@ -160,6 +163,11 @@ function transformPayload(values: ScraperFormValues): CreateScraperPayload {
|
||||
payload.enrich_llm = values.enrich_llm ? 1 : 0;
|
||||
payload.only_match = values.only_match ? 1 : 0;
|
||||
|
||||
// Ne pas envoyer 'once' si non coché
|
||||
if (values.once) {
|
||||
payload.once = 1;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
@ -176,6 +184,7 @@ function createDefaultScraperValues(): ScraperFormValues {
|
||||
enabled: true,
|
||||
enrich_llm: false,
|
||||
only_match: false,
|
||||
once: false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -210,7 +219,10 @@ export function ScrapersTab() {
|
||||
|
||||
const createScraperMutation = useMutation({
|
||||
mutationFn: (values: ScraperFormValues) =>
|
||||
api.post<CreateScraperPayload, Scraper>("/scrapers", transformPayload(values)),
|
||||
api.post<CreateScraperPayload, Scraper>(
|
||||
"/scrapers",
|
||||
transformPayload(values),
|
||||
),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["scrapers"] });
|
||||
setParamsValid(true);
|
||||
@ -253,22 +265,27 @@ export function ScrapersTab() {
|
||||
mutationFn: (values: ScraperFormValues) =>
|
||||
api.post<CreateScraperPayload, ScraperCountResponse>(
|
||||
"/scrapers/count",
|
||||
transformPayload(values)
|
||||
transformPayload(values),
|
||||
),
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit(async (values) => {
|
||||
setStatusMessage(null);
|
||||
if (editingScraperId) {
|
||||
await updateScraperMutation.mutateAsync({ id: editingScraperId, data: values });
|
||||
await updateScraperMutation.mutateAsync({
|
||||
id: editingScraperId,
|
||||
data: values,
|
||||
});
|
||||
} else {
|
||||
await createScraperMutation.mutateAsync(values);
|
||||
}
|
||||
});
|
||||
|
||||
const formError =
|
||||
(createScraperMutation.isError && getErrorMessage(createScraperMutation.error)) ||
|
||||
(updateScraperMutation.isError && getErrorMessage(updateScraperMutation.error)) ||
|
||||
(createScraperMutation.isError &&
|
||||
getErrorMessage(createScraperMutation.error)) ||
|
||||
(updateScraperMutation.isError &&
|
||||
getErrorMessage(updateScraperMutation.error)) ||
|
||||
null;
|
||||
|
||||
const handleOpenCountDialog = async () => {
|
||||
@ -284,7 +301,10 @@ export function ScrapersTab() {
|
||||
countScraperMutation.mutate(form.getValues());
|
||||
};
|
||||
|
||||
const scrapers = useMemo(() => scrapersQuery.data ?? [], [scrapersQuery.data]);
|
||||
const scrapers = useMemo(
|
||||
() => scrapersQuery.data ?? [],
|
||||
[scrapersQuery.data],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@ -292,7 +312,8 @@ export function ScrapersTab() {
|
||||
<CardHeader>
|
||||
<CardTitle>Creer un scraper</CardTitle>
|
||||
<CardDescription>
|
||||
Definissez la configuration et les parametres d'execution du scraper.
|
||||
Definissez la configuration et les parametres d'execution du
|
||||
scraper.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@ -302,6 +323,9 @@ export function ScrapersTab() {
|
||||
<label className="text-sm font-medium" htmlFor="task_name">
|
||||
Nom du scraper
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Identifiant unique pour reperer ce scraper
|
||||
</p>
|
||||
<Input
|
||||
id="task_name"
|
||||
placeholder="Nom interne"
|
||||
@ -313,6 +337,9 @@ export function ScrapersTab() {
|
||||
<label className="text-sm font-medium" htmlFor="frequency">
|
||||
Frequence
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Expression cron pour planifier l'execution
|
||||
</p>
|
||||
<Input
|
||||
id="frequency"
|
||||
placeholder="0 7 * * *"
|
||||
@ -324,55 +351,130 @@ export function ScrapersTab() {
|
||||
<label className="text-sm font-medium" htmlFor="property_types">
|
||||
Types de biens
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Types de proprietes a rechercher (separes par virgule)
|
||||
</p>
|
||||
<Input
|
||||
id="property_types"
|
||||
placeholder="maison,appartement"
|
||||
{...form.register("property_types")}
|
||||
placeholder="maison,appartement,immeuble"
|
||||
aria-invalid={!!form.formState.errors.property_types}
|
||||
{...form.register("property_types", {
|
||||
validate: (value) => {
|
||||
if (!value || value.trim() === "") {
|
||||
return true; // Champ optionnel
|
||||
}
|
||||
const validTypes = ["immeuble", "appartement", "maison"];
|
||||
const types = value
|
||||
.split(",")
|
||||
.map((t) => t.trim().toLowerCase())
|
||||
.filter((t) => t !== "");
|
||||
|
||||
const invalidTypes = types.filter(
|
||||
(t) => !validTypes.includes(t)
|
||||
);
|
||||
|
||||
if (invalidTypes.length > 0) {
|
||||
return `Types invalides: ${invalidTypes.join(", ")}. Valeurs autorisées: ${validTypes.join(", ")}`;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{form.formState.errors.property_types && (
|
||||
<p className="text-destructive text-xs">
|
||||
{form.formState.errors.property_types.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium" htmlFor="last_seen_days">
|
||||
<label
|
||||
className="text-sm font-medium"
|
||||
htmlFor="last_seen_days"
|
||||
>
|
||||
Derniere vue (jours)
|
||||
</label>
|
||||
<Input id="last_seen_days" type="number" min="0" {...form.register("last_seen_days")} />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Annonces vues il y a maximum N jours
|
||||
</p>
|
||||
<Input
|
||||
id="last_seen_days"
|
||||
type="number"
|
||||
min="0"
|
||||
{...form.register("last_seen_days")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium" htmlFor="first_seen_days">
|
||||
<label
|
||||
className="text-sm font-medium"
|
||||
htmlFor="first_seen_days"
|
||||
>
|
||||
Premiere vue (jours)
|
||||
</label>
|
||||
<Input id="first_seen_days" type="number" min="0" {...form.register("first_seen_days")} />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Annonces apparues il y a minimum N jours
|
||||
</p>
|
||||
<Input
|
||||
id="first_seen_days"
|
||||
type="number"
|
||||
min="0"
|
||||
{...form.register("first_seen_days")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium" htmlFor="page_size">
|
||||
Taille de page
|
||||
</label>
|
||||
<Input id="page_size" type="number" min="1" {...form.register("page_size")} />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Nombre de resultats par page
|
||||
</p>
|
||||
<Input
|
||||
id="page_size"
|
||||
type="number"
|
||||
min="1"
|
||||
{...form.register("page_size")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium" htmlFor="max_pages">
|
||||
Pages maximum
|
||||
</label>
|
||||
<Input id="max_pages" type="number" min="1" {...form.register("max_pages")} />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Nombre maximum de pages a parcourir
|
||||
</p>
|
||||
<Input
|
||||
id="max_pages"
|
||||
type="number"
|
||||
min="1"
|
||||
{...form.register("max_pages")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 md:col-span-2 md:grid-cols-3">
|
||||
<div className="grid gap-4 md:col-span-2 md:grid-cols-2">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => field.onChange(Boolean(checked))}
|
||||
/>
|
||||
Activer
|
||||
</label>
|
||||
<div className="space-y-1">
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) =>
|
||||
field.onChange(Boolean(checked))
|
||||
}
|
||||
/>
|
||||
Activer
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground ml-6">
|
||||
Active ou desactive l'execution automatique
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -380,13 +482,20 @@ export function ScrapersTab() {
|
||||
control={form.control}
|
||||
name="enrich_llm"
|
||||
render={({ field }) => (
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => field.onChange(Boolean(checked))}
|
||||
/>
|
||||
Enrichir via LLM
|
||||
</label>
|
||||
<div className="space-y-1">
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) =>
|
||||
field.onChange(Boolean(checked))
|
||||
}
|
||||
/>
|
||||
Enrichir via LLM
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground ml-6">
|
||||
Analyse les annonces avec un modele de langage
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -394,13 +503,41 @@ export function ScrapersTab() {
|
||||
control={form.control}
|
||||
name="only_match"
|
||||
render={({ field }) => (
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => field.onChange(Boolean(checked))}
|
||||
/>
|
||||
Mode strict
|
||||
</label>
|
||||
<div className="space-y-1">
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) =>
|
||||
field.onChange(Boolean(checked))
|
||||
}
|
||||
/>
|
||||
Mode strict
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground ml-6">
|
||||
Ne retourne que les correspondances exactes
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="once"
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) =>
|
||||
field.onChange(Boolean(checked))
|
||||
}
|
||||
/>
|
||||
Une fois
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground ml-6">
|
||||
Execute le scraper une seule fois
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@ -416,7 +553,9 @@ export function ScrapersTab() {
|
||||
onChange={(next) => field.onChange(next)}
|
||||
label="Parametres"
|
||||
description="Structure schemaisee conformement au schema scraper."
|
||||
onValidationChange={(payload) => setParamsValid(payload.valid)}
|
||||
onValidationChange={(payload) =>
|
||||
setParamsValid(payload.valid)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -424,7 +563,11 @@ export function ScrapersTab() {
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={(editingScraperId ? updateScraperMutation.isPending : createScraperMutation.isPending) || !paramsValid}
|
||||
disabled={
|
||||
(editingScraperId
|
||||
? updateScraperMutation.isPending
|
||||
: createScraperMutation.isPending) || !paramsValid
|
||||
}
|
||||
>
|
||||
{editingScraperId
|
||||
? updateScraperMutation.isPending
|
||||
@ -444,7 +587,9 @@ export function ScrapersTab() {
|
||||
) : null}
|
||||
|
||||
{!paramsValid ? (
|
||||
<p className="text-destructive text-sm">Corrigez les erreurs du schema avant l'envoi.</p>
|
||||
<p className="text-destructive text-sm">
|
||||
Corrigez les erreurs du schema avant l'envoi.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
@ -453,7 +598,9 @@ export function ScrapersTab() {
|
||||
disabled={countScraperMutation.isPending}
|
||||
onClick={handleOpenCountDialog}
|
||||
>
|
||||
{countScraperMutation.isPending ? "Calcul..." : "Compter les resultats"}
|
||||
{countScraperMutation.isPending
|
||||
? "Calcul..."
|
||||
: "Compter les resultats"}
|
||||
</Button>
|
||||
|
||||
{editingScraperId ? (
|
||||
@ -496,8 +643,12 @@ export function ScrapersTab() {
|
||||
<thead>
|
||||
<tr className="text-left">
|
||||
<th className="border-b px-3 py-2 font-semibold">Nom</th>
|
||||
<th className="border-b px-3 py-2 font-semibold">Frequence</th>
|
||||
<th className="border-b px-3 py-2 font-semibold">Parametres</th>
|
||||
<th className="border-b px-3 py-2 font-semibold">
|
||||
Frequence
|
||||
</th>
|
||||
<th className="border-b px-3 py-2 font-semibold">
|
||||
Parametres
|
||||
</th>
|
||||
<th className="border-b px-3 py-2 font-semibold">Etat</th>
|
||||
<th className="border-b px-3 py-2 font-semibold">Limites</th>
|
||||
<th className="border-b px-3 py-2 font-semibold">Actions</th>
|
||||
@ -509,7 +660,9 @@ export function ScrapersTab() {
|
||||
|
||||
return (
|
||||
<tr key={scraper.id} className="align-top">
|
||||
<td className="border-b px-3 py-2 font-medium">{scraper.task_name ?? "-"}</td>
|
||||
<td className="border-b px-3 py-2 font-medium">
|
||||
{scraper.task_name ?? "-"}
|
||||
</td>
|
||||
<td className="border-b px-3 py-2 text-muted-foreground">
|
||||
{scraper.frequency ?? "-"}
|
||||
</td>
|
||||
@ -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() {
|
||||
<span className="block text-muted-foreground">
|
||||
Strict: {scraper.only_match ? "Oui" : "Non"}
|
||||
</span>
|
||||
<span className="block text-muted-foreground">
|
||||
Une fois: {scraper.once ? "Oui" : "Non"}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="border-b px-3 py-2 text-xs text-muted-foreground">
|
||||
<div className="space-y-1">
|
||||
<span>Derniere vue: {scraper.last_seen_days ?? "-"}</span>
|
||||
<span>Premiere vue: {scraper.first_seen_days ?? "-"}</span>
|
||||
<span>
|
||||
Derniere vue: {scraper.last_seen_days ?? "-"}
|
||||
</span>
|
||||
<span>
|
||||
Premiere vue: {scraper.first_seen_days ?? "-"}
|
||||
</span>
|
||||
<span>Page size: {scraper.page_size ?? "-"}</span>
|
||||
<span>Max pages: {scraper.max_pages ?? "-"}</span>
|
||||
</div>
|
||||
@ -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() {
|
||||
</DialogHeader>
|
||||
<div className="min-h-[80px]">
|
||||
{countScraperMutation.isPending ? (
|
||||
<p className="text-sm text-muted-foreground">Calcul en cours...</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Calcul en cours...
|
||||
</p>
|
||||
) : countScraperMutation.isError ? (
|
||||
<p className="text-destructive text-sm">
|
||||
{getErrorMessage(countScraperMutation.error)}
|
||||
@ -640,7 +814,10 @@ export function ScrapersTab() {
|
||||
) : countScraperMutation.data ? (
|
||||
<p className="text-sm">
|
||||
Ce scraper correspond a
|
||||
<span className="font-semibold"> {countScraperMutation.data.count} </span>
|
||||
<span className="font-semibold">
|
||||
{" "}
|
||||
{countScraperMutation.data.count}{" "}
|
||||
</span>
|
||||
annonce(s) potentielle(s).
|
||||
</p>
|
||||
) : (
|
||||
|
||||
@ -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<TResponse>(
|
||||
options?: ApiRequestOptions,
|
||||
): Promise<TResponse> {
|
||||
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") ?? "";
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
571
frontend/src/schemas/scraper-schema-labels.ts
Normal file
571
frontend/src/schemas/scraper-schema-labels.ts
Normal file
@ -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<string, FieldMetadata> = {
|
||||
// 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user