recherche
This commit is contained in:
@ -5,9 +5,10 @@ import { Pressable, Text, View } from 'react-native';
|
||||
import { GrillePrixTab } from '@/components/recherche/GrillePrixTab';
|
||||
import { OpportunitesTab } from '@/components/recherche/OpportunitesTab';
|
||||
import { SecteurTab } from '@/components/recherche/SecteurTab';
|
||||
import { VeilleAgentsTab } from '@/components/recherche/VeilleAgentsTab';
|
||||
import { UI } from '@/constants/uiTheme';
|
||||
|
||||
const TABS = ['Secteur', 'Opportunités', 'Grille de prix'] as const;
|
||||
const TABS = ['Secteur', 'Opportunités', 'Grille de prix', 'Veille & agents'] as const;
|
||||
|
||||
export default function RechercheTab() {
|
||||
const [sub, setSub] = useState(0);
|
||||
@ -38,6 +39,7 @@ export default function RechercheTab() {
|
||||
{sub === 0 ? <SecteurTab /> : null}
|
||||
{sub === 1 ? <OpportunitesTab /> : null}
|
||||
{sub === 2 ? <GrillePrixTab /> : null}
|
||||
{sub === 3 ? <VeilleAgentsTab /> : null}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
433
app/components/recherche/VeilleAgentsTab.tsx
Normal file
433
app/components/recherche/VeilleAgentsTab.tsx
Normal file
@ -0,0 +1,433 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Pressable,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import { UI } from '@/constants/uiTheme';
|
||||
import {
|
||||
agentAlertesScan,
|
||||
agentDvf,
|
||||
agentImmobilier,
|
||||
agentMarchand,
|
||||
agentRedaction,
|
||||
agentVeille,
|
||||
} from '@/services/agentsApi';
|
||||
import { getCurrentUserId, pb } from '@/services/pocketbase';
|
||||
import type {
|
||||
AlerteRechercheRecord,
|
||||
AnnonceVeilleRecord,
|
||||
CourrierImmobilierRecord,
|
||||
RechercheSauvegardeeRecord,
|
||||
TransactionSecteurRecord,
|
||||
} from '@/types/collections';
|
||||
import { formatPocketBaseError } from '@/utils/pocketbaseErrors';
|
||||
|
||||
function showLong(title: string, body: string) {
|
||||
Alert.alert(title, body.length > 3500 ? `${body.slice(0, 3500)}…` : body);
|
||||
}
|
||||
|
||||
export function VeilleAgentsTab() {
|
||||
const uid = getCurrentUserId();
|
||||
const qc = useQueryClient();
|
||||
|
||||
const [nomRecherche, setNomRecherche] = useState('');
|
||||
const [critereJson, setCritereJson] = useState('{}');
|
||||
const [titreAnnonce, setTitreAnnonce] = useState('');
|
||||
const [urlAnnonce, setUrlAnnonce] = useState('');
|
||||
const [libelleDvf, setLibelleDvf] = useState('');
|
||||
const [prixM2, setPrixM2] = useState('');
|
||||
const [nbVentes, setNbVentes] = useState('');
|
||||
|
||||
const invalidateVeille = useCallback(() => {
|
||||
void qc.invalidateQueries({ queryKey: ['veille'] });
|
||||
}, [qc]);
|
||||
|
||||
const recherches = useQuery({
|
||||
queryKey: ['veille', 'recherches_sauvegardees', uid],
|
||||
queryFn: () =>
|
||||
pb.collection('recherches_sauvegardees').getFullList<RechercheSauvegardeeRecord>({ sort: '-updated' }),
|
||||
enabled: Boolean(uid),
|
||||
});
|
||||
|
||||
const alertes = useQuery({
|
||||
queryKey: ['veille', 'alertes_recherche', uid],
|
||||
queryFn: () => pb.collection('alertes_recherche').getFullList<AlerteRechercheRecord>({ sort: '-updated' }),
|
||||
enabled: Boolean(uid),
|
||||
});
|
||||
|
||||
const annonces = useQuery({
|
||||
queryKey: ['veille', 'annonces_veille', uid],
|
||||
queryFn: () => pb.collection('annonces_veille').getFullList<AnnonceVeilleRecord>({ sort: '-updated' }),
|
||||
enabled: Boolean(uid),
|
||||
});
|
||||
|
||||
const trans = useQuery({
|
||||
queryKey: ['veille', 'transactions_secteur', uid],
|
||||
queryFn: () => pb.collection('transactions_secteur').getFullList<TransactionSecteurRecord>({ sort: '-updated' }),
|
||||
enabled: Boolean(uid),
|
||||
});
|
||||
|
||||
const courriers = useQuery({
|
||||
queryKey: ['veille', 'courriers_immobilier', uid],
|
||||
queryFn: () => pb.collection('courriers_immobilier').getFullList<CourrierImmobilierRecord>({ sort: '-updated' }),
|
||||
enabled: Boolean(uid),
|
||||
});
|
||||
|
||||
const createRecherche = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!uid) throw new Error('Non connecté');
|
||||
const nom = nomRecherche.trim();
|
||||
if (!nom) throw new Error('Nom requis');
|
||||
return pb.collection('recherches_sauvegardees').create<RechercheSauvegardeeRecord>({
|
||||
user: uid,
|
||||
nom,
|
||||
critere_json: critereJson.trim() || '{}',
|
||||
actif: true,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
setNomRecherche('');
|
||||
invalidateVeille();
|
||||
},
|
||||
});
|
||||
|
||||
const createAlerteSimple = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!uid) throw new Error('Non connecté');
|
||||
return pb.collection('alertes_recherche').create<AlerteRechercheRecord>({
|
||||
user: uid,
|
||||
nom: `Veille ${new Date().toLocaleDateString('fr-FR')}`,
|
||||
canal: 'in_app',
|
||||
actif: true,
|
||||
});
|
||||
},
|
||||
onSuccess: invalidateVeille,
|
||||
onError: (err) => Alert.alert('Erreur', formatPocketBaseError(err)),
|
||||
});
|
||||
|
||||
const scanAlertes = useMutation({
|
||||
mutationFn: () => agentAlertesScan(),
|
||||
onSuccess: (r) => {
|
||||
invalidateVeille();
|
||||
Alert.alert('Scan alertes', `${r.processed} alertes mises à jour.\n${r.note ?? ''}`);
|
||||
},
|
||||
onError: (e) => Alert.alert('Erreur', formatPocketBaseError(e)),
|
||||
});
|
||||
|
||||
const pushAnnonce = useMutation({
|
||||
mutationFn: () =>
|
||||
agentVeille({
|
||||
titre: titreAnnonce.trim(),
|
||||
url: urlAnnonce.trim() || undefined,
|
||||
source: 'manuel',
|
||||
}),
|
||||
onSuccess: (r) => {
|
||||
setTitreAnnonce('');
|
||||
setUrlAnnonce('');
|
||||
invalidateVeille();
|
||||
Alert.alert('Veille', r.dedupe ? 'Doublon ignoré.' : `Enregistrée (${r.id}).`);
|
||||
},
|
||||
onError: (e) => Alert.alert('Erreur', formatPocketBaseError(e)),
|
||||
});
|
||||
|
||||
const pushDvf = useMutation({
|
||||
mutationFn: () => {
|
||||
const lib = libelleDvf.trim();
|
||||
if (!lib) throw new Error('Libellé requis');
|
||||
const pm = Number(String(prixM2).replace(',', '.'));
|
||||
const nv = Number(String(nbVentes).trim());
|
||||
return agentDvf({
|
||||
libelle: lib,
|
||||
prix_m2_median: Number.isFinite(pm) ? pm : undefined,
|
||||
nb_ventes: Number.isFinite(nv) ? nv : undefined,
|
||||
});
|
||||
},
|
||||
onSuccess: (r) => {
|
||||
setLibelleDvf('');
|
||||
setPrixM2('');
|
||||
setNbVentes('');
|
||||
invalidateVeille();
|
||||
showLong('Synthèse marché', r.synthese);
|
||||
},
|
||||
onError: (e) => Alert.alert('Erreur', formatPocketBaseError(e)),
|
||||
});
|
||||
|
||||
const runImmobilier = useMutation({
|
||||
mutationFn: () =>
|
||||
agentImmobilier({
|
||||
objectif: 'Prospection ciblée secteur',
|
||||
contexte: 'Génère un plan + un message court pour relancer des mandataires potentiels.',
|
||||
save: false,
|
||||
}),
|
||||
onSuccess: (r) => showLong('Agent immobilier', r.brouillon),
|
||||
onError: (e) => Alert.alert('Erreur', formatPocketBaseError(e)),
|
||||
});
|
||||
|
||||
const runMarchand = useMutation({
|
||||
mutationFn: () =>
|
||||
agentMarchand({
|
||||
titre: titreAnnonce.trim() || 'Annonce test',
|
||||
notes: 'Comparer avec ma grille perso (onglet Grille de prix).',
|
||||
}),
|
||||
onSuccess: (r) => showLong('Agent marchand de biens', r.analyse),
|
||||
onError: (e) => Alert.alert('Erreur', formatPocketBaseError(e)),
|
||||
});
|
||||
|
||||
const runRedaction = useMutation({
|
||||
mutationFn: () =>
|
||||
agentRedaction({
|
||||
kind: 'annonce_agence',
|
||||
bullets: ['Lumineux', 'Proche transports', 'Charges faibles'],
|
||||
save: false,
|
||||
}),
|
||||
onSuccess: (r) => showLong('Agent rédaction', r.texte),
|
||||
onError: (e) => Alert.alert('Erreur', formatPocketBaseError(e)),
|
||||
});
|
||||
|
||||
const loading =
|
||||
recherches.isPending ||
|
||||
alertes.isPending ||
|
||||
annonces.isPending ||
|
||||
trans.isPending ||
|
||||
courriers.isPending;
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
className="flex-1 px-4 py-3"
|
||||
style={{ backgroundColor: UI.screen }}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={loading}
|
||||
onRefresh={() => {
|
||||
void recherches.refetch();
|
||||
void alertes.refetch();
|
||||
void annonces.refetch();
|
||||
void trans.refetch();
|
||||
void courriers.refetch();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Text className="mb-2 text-lg font-bold" style={{ color: UI.text }}>
|
||||
Agents IA (MVP)
|
||||
</Text>
|
||||
<Text className="mb-4 text-sm" style={{ color: UI.textMuted }}>
|
||||
Connexion serveur + clé Anthropic requises. Les données restent dans PocketBase.
|
||||
</Text>
|
||||
|
||||
<View className="mb-6 gap-2">
|
||||
<Text className="font-semibold" style={{ color: UI.text }}>
|
||||
Lancer un agent
|
||||
</Text>
|
||||
<View className="flex-row flex-wrap gap-2">
|
||||
<AgentButton label="Immobilier" loading={runImmobilier.isPending} onPress={() => runImmobilier.mutate()} />
|
||||
<AgentButton
|
||||
label="Marchand"
|
||||
loading={runMarchand.isPending}
|
||||
onPress={() => runMarchand.mutate()}
|
||||
/>
|
||||
<AgentButton label="Rédaction" loading={runRedaction.isPending} onPress={() => runRedaction.mutate()} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="mb-6 rounded-lg border p-3" style={{ borderColor: UI.border, backgroundColor: UI.card }}>
|
||||
<Text className="mb-2 font-semibold" style={{ color: UI.text }}>
|
||||
Agent data / secteur (stub DVF)
|
||||
</Text>
|
||||
<TextInput
|
||||
className="mb-2 rounded border px-2 py-2"
|
||||
style={{ borderColor: UI.border, color: UI.text }}
|
||||
placeholder="Libellé zone (ex. Lyon 6e)"
|
||||
placeholderTextColor={UI.textMuted}
|
||||
value={libelleDvf}
|
||||
onChangeText={setLibelleDvf}
|
||||
/>
|
||||
<View className="mb-2 flex-row gap-2">
|
||||
<TextInput
|
||||
className="flex-1 rounded border px-2 py-2"
|
||||
style={{ borderColor: UI.border, color: UI.text }}
|
||||
placeholder="Prix m² médian"
|
||||
placeholderTextColor={UI.textMuted}
|
||||
keyboardType="decimal-pad"
|
||||
value={prixM2}
|
||||
onChangeText={setPrixM2}
|
||||
/>
|
||||
<TextInput
|
||||
className="flex-1 rounded border px-2 py-2"
|
||||
style={{ borderColor: UI.border, color: UI.text }}
|
||||
placeholder="Nb ventes"
|
||||
placeholderTextColor={UI.textMuted}
|
||||
keyboardType="number-pad"
|
||||
value={nbVentes}
|
||||
onChangeText={setNbVentes}
|
||||
/>
|
||||
</View>
|
||||
<Pressable
|
||||
className="items-center rounded-lg py-3"
|
||||
style={{ backgroundColor: UI.primary }}
|
||||
onPress={() => pushDvf.mutate()}
|
||||
disabled={pushDvf.isPending || !libelleDvf.trim()}
|
||||
>
|
||||
{pushDvf.isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text className="font-semibold text-white">Enregistrer + synthèse IA</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View className="mb-6 rounded-lg border p-3" style={{ borderColor: UI.border, backgroundColor: UI.card }}>
|
||||
<Text className="mb-2 font-semibold" style={{ color: UI.text }}>
|
||||
Agent veille (dédoublonnage MD5)
|
||||
</Text>
|
||||
<TextInput
|
||||
className="mb-2 rounded border px-2 py-2"
|
||||
style={{ borderColor: UI.border, color: UI.text }}
|
||||
placeholder="Titre annonce"
|
||||
placeholderTextColor={UI.textMuted}
|
||||
value={titreAnnonce}
|
||||
onChangeText={setTitreAnnonce}
|
||||
/>
|
||||
<TextInput
|
||||
className="mb-2 rounded border px-2 py-2"
|
||||
style={{ borderColor: UI.border, color: UI.text }}
|
||||
placeholder="URL (optionnel)"
|
||||
placeholderTextColor={UI.textMuted}
|
||||
value={urlAnnonce}
|
||||
onChangeText={setUrlAnnonce}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<Pressable
|
||||
className="items-center rounded-lg py-3"
|
||||
style={{ backgroundColor: UI.primary }}
|
||||
onPress={() => pushAnnonce.mutate()}
|
||||
disabled={pushAnnonce.isPending || !titreAnnonce.trim()}
|
||||
>
|
||||
{pushAnnonce.isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text className="font-semibold text-white">Ajouter à la veille</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View className="mb-6 rounded-lg border p-3" style={{ borderColor: UI.border, backgroundColor: UI.card }}>
|
||||
<Text className="mb-2 font-semibold" style={{ color: UI.text }}>
|
||||
Recherches sauvegardées
|
||||
</Text>
|
||||
<TextInput
|
||||
className="mb-2 rounded border px-2 py-2"
|
||||
style={{ borderColor: UI.border, color: UI.text }}
|
||||
placeholder="Nom"
|
||||
placeholderTextColor={UI.textMuted}
|
||||
value={nomRecherche}
|
||||
onChangeText={setNomRecherche}
|
||||
/>
|
||||
<TextInput
|
||||
className="mb-2 rounded border px-2 py-2"
|
||||
style={{ borderColor: UI.border, color: UI.text }}
|
||||
placeholder='Critères JSON (ex. {"prix_max":250000})'
|
||||
placeholderTextColor={UI.textMuted}
|
||||
value={critereJson}
|
||||
onChangeText={setCritereJson}
|
||||
multiline
|
||||
/>
|
||||
<Pressable
|
||||
className="mb-3 items-center rounded-lg py-2"
|
||||
style={{ backgroundColor: UI.border }}
|
||||
onPress={() => createRecherche.mutate()}
|
||||
disabled={createRecherche.isPending}
|
||||
>
|
||||
<Text style={{ color: UI.text }}>Créer la recherche</Text>
|
||||
</Pressable>
|
||||
{recherches.data?.map((r) => (
|
||||
<Text key={r.id} className="text-sm" style={{ color: UI.textMuted }}>
|
||||
• {r.nom}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View className="mb-6 rounded-lg border p-3" style={{ borderColor: UI.border, backgroundColor: UI.card }}>
|
||||
<Text className="mb-2 font-semibold" style={{ color: UI.text }}>
|
||||
Agent alertes (stub scan)
|
||||
</Text>
|
||||
<Pressable
|
||||
className="mb-2 items-center rounded-lg py-2"
|
||||
style={{ backgroundColor: UI.border }}
|
||||
onPress={() => createAlerteSimple.mutate()}
|
||||
disabled={createAlerteSimple.isPending}
|
||||
>
|
||||
<Text style={{ color: UI.text }}>Nouvelle alerte in-app</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
className="items-center rounded-lg py-2"
|
||||
style={{ backgroundColor: UI.border }}
|
||||
onPress={() => scanAlertes.mutate()}
|
||||
disabled={scanAlertes.isPending}
|
||||
>
|
||||
{scanAlertes.isPending ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<Text style={{ color: UI.text }}>Scanner mes alertes actives</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
{alertes.data?.map((a) => (
|
||||
<Text key={a.id} className="mt-1 text-sm" style={{ color: UI.textMuted }}>
|
||||
• {a.nom} ({a.canal}) {a.actif === false ? '— off' : ''}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text className="mb-1 font-semibold" style={{ color: UI.text }}>
|
||||
Annonces veille ({annonces.data?.length ?? 0})
|
||||
</Text>
|
||||
{annonces.data?.slice(0, 8).map((a) => (
|
||||
<Text key={a.id} className="text-sm" style={{ color: UI.textMuted }}>
|
||||
[{a.statut}] {a.titre}
|
||||
</Text>
|
||||
))}
|
||||
|
||||
<Text className="mb-1 mt-4 font-semibold" style={{ color: UI.text }}>
|
||||
Transactions secteur ({trans.data?.length ?? 0})
|
||||
</Text>
|
||||
{trans.data?.slice(0, 6).map((t) => (
|
||||
<Text key={t.id} className="text-sm" style={{ color: UI.textMuted }}>
|
||||
{t.libelle}
|
||||
{t.prix_m2_median != null ? ` — ${t.prix_m2_median} €/m²` : ''}
|
||||
</Text>
|
||||
))}
|
||||
|
||||
<Text className="mb-1 mt-4 font-semibold" style={{ color: UI.text }}>
|
||||
Courriers ({courriers.data?.length ?? 0})
|
||||
</Text>
|
||||
{courriers.data?.slice(0, 5).map((c) => (
|
||||
<Text key={c.id} className="text-sm" style={{ color: UI.textMuted }}>
|
||||
{c.titre} ({c.kind}/{c.etat})
|
||||
</Text>
|
||||
))}
|
||||
|
||||
<View className="h-24" />
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentButton(props: { label: string; loading: boolean; onPress: () => void }) {
|
||||
return (
|
||||
<Pressable
|
||||
className="rounded-lg px-4 py-2"
|
||||
style={{ backgroundColor: UI.primary }}
|
||||
onPress={props.onPress}
|
||||
disabled={props.loading}
|
||||
>
|
||||
{props.loading ? <ActivityIndicator color="#fff" size="small" /> : <Text className="text-white">{props.label}</Text>}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
66
app/services/agentsApi.ts
Normal file
66
app/services/agentsApi.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { pb } from '@/services/pocketbase';
|
||||
|
||||
export type AgentImmobilierBody = {
|
||||
objectif?: string;
|
||||
contexte?: string;
|
||||
save?: boolean;
|
||||
};
|
||||
|
||||
export type AgentMarchandBody = {
|
||||
titre?: string;
|
||||
prix?: number;
|
||||
surface?: number;
|
||||
code_postal?: string;
|
||||
ville?: string;
|
||||
notes?: string;
|
||||
grille_json?: string;
|
||||
};
|
||||
|
||||
export type AgentDvfBody = {
|
||||
libelle: string;
|
||||
code_insee?: string;
|
||||
annee?: number;
|
||||
prix_m2_median?: number;
|
||||
nb_ventes?: number;
|
||||
detail_json?: string;
|
||||
};
|
||||
|
||||
export type AgentVeilleBody = {
|
||||
titre: string;
|
||||
url?: string;
|
||||
source?: string;
|
||||
prix?: number;
|
||||
surface?: number;
|
||||
code_postal?: string;
|
||||
ville?: string;
|
||||
};
|
||||
|
||||
export type AgentRedactionBody = {
|
||||
kind?: string;
|
||||
bullets?: string[];
|
||||
save?: boolean;
|
||||
};
|
||||
|
||||
export async function agentImmobilier(body: AgentImmobilierBody): Promise<{ brouillon: string; courrier_id?: string }> {
|
||||
return pb.send('/api/mdb/agent-immobilier', { method: 'POST', body });
|
||||
}
|
||||
|
||||
export async function agentMarchand(body: AgentMarchandBody): Promise<{ analyse: string }> {
|
||||
return pb.send('/api/mdb/agent-marchand', { method: 'POST', body });
|
||||
}
|
||||
|
||||
export async function agentDvf(body: AgentDvfBody): Promise<{ id: string; synthese: string }> {
|
||||
return pb.send('/api/mdb/agent-dvf', { method: 'POST', body });
|
||||
}
|
||||
|
||||
export async function agentVeille(body: AgentVeilleBody): Promise<{ id: string; dedupe: boolean; message?: string }> {
|
||||
return pb.send('/api/mdb/agent-veille', { method: 'POST', body });
|
||||
}
|
||||
|
||||
export async function agentRedaction(body: AgentRedactionBody): Promise<{ texte: string; courrier_id?: string }> {
|
||||
return pb.send('/api/mdb/agent-redaction', { method: 'POST', body });
|
||||
}
|
||||
|
||||
export async function agentAlertesScan(): Promise<{ processed: number; note?: string }> {
|
||||
return pb.send('/api/mdb/agent-alertes-scan', { method: 'POST', body: {} });
|
||||
}
|
||||
@ -146,6 +146,72 @@ export type GrillePrixRecord = RecordModel & {
|
||||
ville?: string;
|
||||
};
|
||||
|
||||
export type RechercheSauvegardeeRecord = RecordModel & {
|
||||
user: string;
|
||||
nom: string;
|
||||
critere_json?: string;
|
||||
actif?: boolean;
|
||||
};
|
||||
|
||||
export type AlerteRechercheRecord = RecordModel & {
|
||||
user: string;
|
||||
recherche?: string;
|
||||
nom: string;
|
||||
canal: 'in_app' | 'email' | 'push';
|
||||
actif?: boolean;
|
||||
derniere_verification?: string;
|
||||
dernier_nb_resultats?: number;
|
||||
};
|
||||
|
||||
export type AnnonceVeilleStatut = 'nouveau' | 'vu' | 'ecarte' | 'raccroche';
|
||||
|
||||
export type AnnonceVeilleRecord = RecordModel & {
|
||||
user: string;
|
||||
titre: string;
|
||||
url?: string;
|
||||
source?: string;
|
||||
prix?: number;
|
||||
surface?: number;
|
||||
code_postal?: string;
|
||||
ville?: string;
|
||||
empreinte?: string;
|
||||
statut: AnnonceVeilleStatut;
|
||||
};
|
||||
|
||||
export type FluxSourceType = 'api' | 'manuel' | 'csv';
|
||||
|
||||
export type FluxSourceRecord = RecordModel & {
|
||||
user: string;
|
||||
nom: string;
|
||||
type: FluxSourceType;
|
||||
notes?: string;
|
||||
actif?: boolean;
|
||||
};
|
||||
|
||||
export type TransactionSecteurSource = 'manuel' | 'dvf_import' | 'api_tiers';
|
||||
|
||||
export type TransactionSecteurRecord = RecordModel & {
|
||||
user: string;
|
||||
libelle: string;
|
||||
code_insee?: string;
|
||||
annee?: number;
|
||||
prix_m2_median?: number;
|
||||
nb_ventes?: number;
|
||||
source: TransactionSecteurSource;
|
||||
detail_json?: string;
|
||||
};
|
||||
|
||||
export type CourrierImmobilierKind = 'prospection' | 'annonce_agence' | 'relance';
|
||||
export type CourrierImmobilierEtat = 'brouillon' | 'pret';
|
||||
|
||||
export type CourrierImmobilierRecord = RecordModel & {
|
||||
user: string;
|
||||
titre: string;
|
||||
corps?: string;
|
||||
kind: CourrierImmobilierKind;
|
||||
etat: CourrierImmobilierEtat;
|
||||
};
|
||||
|
||||
export type VisiteRecord = RecordModel & {
|
||||
user: string;
|
||||
bien: string;
|
||||
|
||||
Reference in New Issue
Block a user