Files
mdb/app/components/recherche/OpportunitesTab.tsx
Bastien COIGNOUX 2b8741de08 recherche
2026-05-04 21:52:51 +02:00

285 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Ionicons } from '@expo/vector-icons';
import * as Clipboard from 'expo-clipboard';
import type { ReactNode } from 'react';
import { useCallback, useState } from 'react';
import {
ActivityIndicator,
Alert,
Linking,
Pressable,
ScrollView,
Text,
TextInput,
View,
} from 'react-native';
import {
LEGI_L151_36,
LEGI_L152_6,
OFF_MARKET_KEYWORDS,
PROSPECTION_CHECKLIST,
} from '@/constants/rechercheMarche';
import { UI } from '@/constants/uiTheme';
import { useNotesProspectionRecherche } from '@/hooks/useNotesProspectionRecherche';
import { formatPocketBaseError } from '@/utils/pocketbaseErrors';
const LBC = 'https://www.leboncoin.fr/';
const MOTEUR = 'https://www.moteurimmo.fr/';
function Collapsible({
title,
children,
defaultOpen,
}: {
title: string;
children: ReactNode;
defaultOpen?: boolean;
}) {
const [open, setOpen] = useState(Boolean(defaultOpen));
return (
<View className="mb-3 rounded-2xl border-2 bg-white" style={{ borderColor: UI.border }}>
<Pressable
accessibilityRole="button"
onPress={() => setOpen((o) => !o)}
className="min-h-[52px] flex-row items-center justify-between px-4 py-3"
>
<Text className="flex-1 pr-2 text-lg font-bold" style={{ color: UI.text }}>
{title}
</Text>
<Ionicons name={open ? 'chevron-up' : 'chevron-down'} size={22} color={UI.textMuted} />
</Pressable>
{open ? <View className="border-t-2 px-4 pb-4 pt-2" style={{ borderColor: UI.border }}>{children}</View> : null}
</View>
);
}
export function OpportunitesTab() {
const { getState, saveChecklistItem, isLoading, isSaving } = useNotesProspectionRecherche();
const [expandedNoteId, setExpandedNoteId] = useState<string | null>(null);
const [noteDraft, setNoteDraft] = useState('');
const persistItem = useCallback(
async (questionId: string, next: { done: boolean; note: string }) => {
try {
await saveChecklistItem({ questionId, data: next });
} catch (e) {
Alert.alert('Sauvegarde', formatPocketBaseError(e));
}
},
[saveChecklistItem],
);
const openNoteEditor = (questionId: string) => {
const st = getState(questionId);
setExpandedNoteId(questionId);
setNoteDraft(st.note);
};
const saveNoteFor = async (questionId: string) => {
const st = getState(questionId);
await persistItem(questionId, { done: st.done, note: noteDraft.trim() });
setExpandedNoteId(null);
};
const copyText = async (text: string) => {
try {
await Clipboard.setStringAsync(text);
Alert.alert('Copié', 'Collage dans Leboncoin ou Moteur Immo.');
} catch {
Alert.alert('Presse-papiers', 'Copie impossible sur cet appareil.');
}
};
if (isLoading) {
return (
<View className="flex-1 items-center justify-center py-16">
<ActivityIndicator size="large" color={UI.primary} />
</View>
);
}
return (
<ScrollView className="flex-1 px-3 pt-2" contentContainerStyle={{ paddingBottom: 120 }}>
<Collapsible title="1. Mots-clés off-market">
{OFF_MARKET_KEYWORDS.map((k) => (
<View key={k.id} className="mb-3 rounded-xl border-2 p-3" style={{ borderColor: UI.border }}>
<Text className="text-base font-bold" style={{ color: UI.text }}>
{k.label}
</Text>
<Text selectable className="mt-2 font-mono text-sm leading-5" style={{ color: UI.text }}>
{k.text}
</Text>
<Pressable
onPress={() => void copyText(k.text)}
className="mt-3 min-h-[48px] items-center justify-center rounded-xl"
style={{ backgroundColor: UI.primary }}
>
<Text className="text-base font-bold text-white">Copier</Text>
</Pressable>
</View>
))}
<View className="mt-2 flex-row flex-wrap gap-3">
<Pressable
onPress={() => void Linking.openURL(LBC)}
className="min-h-[52px] flex-1 min-w-[140px] items-center justify-center rounded-2xl border-2"
style={{ borderColor: UI.warning, backgroundColor: '#FFFBEB' }}
>
<Text className="text-lg font-bold" style={{ color: '#B45309' }}>
Leboncoin
</Text>
</Pressable>
<Pressable
onPress={() => void Linking.openURL(MOTEUR)}
className="min-h-[52px] flex-1 min-w-[140px] items-center justify-center rounded-2xl border-2"
style={{ borderColor: UI.warning, backgroundColor: '#FFFBEB' }}
>
<Text className="text-lg font-bold" style={{ color: '#B45309' }}>
Moteur Immo
</Text>
</Pressable>
</View>
<View className="mt-4 rounded-2xl border-2 p-4" style={{ borderColor: UI.primary, backgroundColor: '#EFF6FF' }}>
<Text className="text-base font-bold" style={{ color: UI.primary }}>
Astuce maison de ville
</Text>
<Text className="mt-2 text-base leading-6" style={{ color: UI.text }}>
Surface terrain max 100 m² + surface habitable min 150 m² maisons sans jardin, moins de concurrence,
idéal division.
</Text>
</View>
</Collapsible>
<Collapsible title='2. Biens "fatigués" — décotes'>
<Text className="text-base leading-6" style={{ color: UI.text }}>
Trier par ancienneté sur Moteur Immo. 40+ mois en ligne + baisses répétées = vendeur motivé. Décote possible
sous prix marché.
</Text>
<View className="mt-3 self-start rounded-xl px-3 py-2" style={{ backgroundColor: '#FEE2E2' }}>
<Text className="text-lg font-bold" style={{ color: UI.danger }}>
10 % à 24 %
</Text>
</View>
<Pressable
onPress={() => void Linking.openURL(MOTEUR)}
className="mt-4 min-h-[52px] items-center justify-center rounded-2xl"
style={{ backgroundColor: UI.danger }}
>
<Text className="text-center text-lg font-bold text-white">Moteur Immo tri par ancienneté</Text>
</Pressable>
</Collapsible>
<Collapsible title="3. Vérifier la division — articles">
<View className="mb-3 rounded-2xl border-2 p-4" style={{ borderColor: UI.primary, backgroundColor: '#EFF6FF' }}>
<Text className="text-lg font-bold" style={{ color: UI.primary }}>
Article L151-36
</Text>
<Text className="mt-2 text-base leading-6" style={{ color: UI.text }}>
1 place de parking max par logement créé en zone bien desservie.
</Text>
<Pressable
onPress={() => void Linking.openURL(LEGI_L151_36)}
className="mt-3 min-h-[48px] items-center justify-center rounded-xl bg-white"
>
<Text className="text-base font-bold" style={{ color: UI.primary }}>
Ouvrir sur Légifrance
</Text>
</Pressable>
</View>
<View className="rounded-2xl border-2 p-4" style={{ borderColor: UI.success, backgroundColor: '#F0FDF4' }}>
<Text className="text-lg font-bold" style={{ color: UI.success }}>
Article L152-6
</Text>
<Text className="mt-2 text-base leading-6" style={{ color: UI.text }}>
Dans 500 m d&apos;une gare ou métro division sans obligation parking.
</Text>
<Pressable
onPress={() => void Linking.openURL(LEGI_L152_6)}
className="mt-3 min-h-[48px] items-center justify-center rounded-xl bg-white"
>
<Text className="text-base font-bold" style={{ color: UI.success }}>
Ouvrir sur Légifrance
</Text>
</Pressable>
</View>
</Collapsible>
<Collapsible title="4. Confirmer avec les pros">
<Text className="mb-2 text-base" style={{ color: UI.textMuted }}>
Coche après échange ; note la réponse pour ton dossier.
</Text>
{PROSPECTION_CHECKLIST.map((item) => {
const st = getState(item.id);
const expanded = expandedNoteId === item.id;
return (
<View key={item.id} className="mb-3 rounded-xl border-2 px-3 py-3" style={{ borderColor: UI.border }}>
<Pressable
accessibilityRole="checkbox"
accessibilityState={{ checked: st.done }}
onPress={() => void persistItem(item.id, { done: !st.done, note: st.note })}
className="min-h-[48px] flex-row items-start gap-3"
>
<View
className="mt-0.5 h-8 w-8 items-center justify-center rounded-lg border-2"
style={{
borderColor: st.done ? UI.success : UI.border,
backgroundColor: st.done ? UI.success : UI.card,
}}
>
{st.done ? <Text className="font-bold text-white"></Text> : null}
</View>
<View className="min-w-0 flex-1">
<Text className="text-base font-bold" style={{ color: UI.textMuted }}>
{item.label}
</Text>
<Text className="mt-1 text-base leading-6" style={{ color: UI.text }}>
{item.question}
</Text>
</View>
</Pressable>
<Pressable
onPress={() => (expanded ? setExpandedNoteId(null) : openNoteEditor(item.id))}
className="mt-3 min-h-[44px] justify-center rounded-xl px-3"
style={{ backgroundColor: '#F1F5F9' }}
>
<Text className="text-base font-semibold" style={{ color: UI.primary }}>
{expanded ? 'Fermer la note' : st.note ? 'Modifier la note' : 'Ajouter une note'}
</Text>
</Pressable>
{expanded ? (
<View className="mt-2">
<TextInput
className="min-h-[88px] rounded-xl border-2 px-3 py-2 text-base"
style={{ borderColor: UI.border, color: UI.text }}
multiline
textAlignVertical="top"
value={noteDraft}
onChangeText={setNoteDraft}
placeholder="Réponse du pro…"
placeholderTextColor={UI.textMuted}
/>
<Pressable
onPress={() => void saveNoteFor(item.id)}
disabled={isSaving}
className="mt-2 min-h-[48px] items-center justify-center rounded-xl"
style={{ backgroundColor: UI.primary }}
>
{isSaving ? (
<ActivityIndicator color="#fff" />
) : (
<Text className="text-base font-bold text-white">Enregistrer la note</Text>
)}
</Pressable>
</View>
) : st.note ? (
<Text className="mt-2 text-base italic" style={{ color: UI.textMuted }}>
{st.note}
</Text>
) : null}
</View>
);
})}
</Collapsible>
</ScrollView>
);
}