147 lines
4.7 KiB
TypeScript
147 lines
4.7 KiB
TypeScript
import { router } from 'expo-router';
|
||
import { useMemo, useState } from 'react';
|
||
import {
|
||
Pressable,
|
||
ScrollView,
|
||
StyleSheet,
|
||
Text,
|
||
TextInput,
|
||
View,
|
||
} from 'react-native';
|
||
import { runFinancialAgent } from '../src/agents/orchestrator';
|
||
import { analyzeDeal, RED_FLAG_MARGIN_PCT, type DealAnalysisInput } from '../src/core/dealAnalysis';
|
||
import { sharePurchaseOfferPdf } from '../src/services/offerPdf';
|
||
import { colors } from '../src/theme/colors';
|
||
|
||
const DEMO_PROPERTY_ID = 'local-demo';
|
||
|
||
export default function RedFlagDashboard() {
|
||
const [asking, setAsking] = useState('185000');
|
||
const [resale, setResale] = useState('268000');
|
||
const [surface, setSurface] = useState('88');
|
||
const [works, setWorks] = useState('42000');
|
||
|
||
const input: DealAnalysisInput = useMemo(
|
||
() => ({
|
||
resaleEstimateTtc: Number(resale.replace(/\s/g, '')) || 0,
|
||
surfaceM2: Number(surface.replace(',', '.')) || 1,
|
||
worksTotalEur: Number(works.replace(/\s/g, '')) || 0,
|
||
notaryRateOnPurchase: 0.077,
|
||
saleAgencyRateOnResale: 0.05,
|
||
miscAcquisitionEur: 2500,
|
||
miscSaleEur: 1800,
|
||
holdingMonths: 6,
|
||
holdingAnnualRateOnPrincipal: 0.055,
|
||
}),
|
||
[resale, surface, works],
|
||
);
|
||
|
||
const analysis = useMemo(() => analyzeDeal(input), [input]);
|
||
const askNum = Number(asking.replace(/\s/g, '')) || 0;
|
||
const fin = useMemo(
|
||
() => runFinancialAgent(input, askNum),
|
||
[input, askNum],
|
||
);
|
||
const red = analysis.isRedFlag(askNum);
|
||
|
||
return (
|
||
<ScrollView contentContainerStyle={styles.scroll}>
|
||
<View style={[styles.banner, red ? styles.bannerBad : styles.bannerOk]}>
|
||
<Text style={styles.bannerTitle}>{red ? 'RED FLAG' : 'GO'}</Text>
|
||
<Text style={styles.bannerSub}>
|
||
Marge nette à prix affiché : {(analysis.netMarginPct(askNum) * 100).toFixed(1)} %
|
||
{' — '}seuil {(RED_FLAG_MARGIN_PCT * 100).toFixed(0)} %
|
||
</Text>
|
||
<Text style={styles.bannerSub}>
|
||
Prix d’achat max (algo) :{' '}
|
||
{fin.maxBuyingPriceEur.toLocaleString('fr-FR')} €
|
||
</Text>
|
||
</View>
|
||
|
||
<Text style={styles.h}>Hypothèses deal</Text>
|
||
<Field label="Prix affiché / offre actuelle (€)" value={asking} onChange={setAsking} />
|
||
<Field label="Prix de revente estimé TTC (€)" value={resale} onChange={setResale} />
|
||
<Field label="Surface (m²)" value={surface} onChange={setSurface} />
|
||
<Field label="Travaux estimés (€)" value={works} onChange={setWorks} />
|
||
|
||
<Pressable style={styles.btn} onPress={() => router.push('/field')}>
|
||
<Text style={styles.btnText}>Field visit checklist</Text>
|
||
</Pressable>
|
||
<Pressable
|
||
style={[styles.btn, styles.btnGhost]}
|
||
onPress={() =>
|
||
void sharePurchaseOfferPdf({
|
||
propertyTitle: 'Bien cible',
|
||
address: 'À compléter',
|
||
maxBuyPriceEur: fin.maxBuyingPriceEur,
|
||
})
|
||
}
|
||
>
|
||
<Text style={styles.btnTextGhost}>One-click offer (PDF)</Text>
|
||
</Pressable>
|
||
|
||
<Text style={styles.foot}>
|
||
Agents SCOUT / ENGINEER / APIs : brancher Edge Functions + clés serveur.
|
||
Dossier checklist : id « {DEMO_PROPERTY_ID} ».
|
||
</Text>
|
||
</ScrollView>
|
||
);
|
||
}
|
||
|
||
function Field({
|
||
label,
|
||
value,
|
||
onChange,
|
||
}: {
|
||
label: string;
|
||
value: string;
|
||
onChange: (s: string) => void;
|
||
}) {
|
||
return (
|
||
<View style={{ marginBottom: 12 }}>
|
||
<Text style={styles.lab}>{label}</Text>
|
||
<TextInput
|
||
keyboardType="decimal-pad"
|
||
value={value}
|
||
onChangeText={onChange}
|
||
style={styles.inp}
|
||
placeholderTextColor={colors.muted}
|
||
/>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
scroll: { padding: 16, paddingBottom: 48 },
|
||
banner: { borderRadius: 16, padding: 18, marginBottom: 20 },
|
||
bannerBad: { backgroundColor: '#3a121c' },
|
||
bannerOk: { backgroundColor: '#0f2a1c' },
|
||
bannerTitle: { color: colors.text, fontSize: 28, fontWeight: '900' },
|
||
bannerSub: { color: colors.muted, marginTop: 8, lineHeight: 20 },
|
||
h: { color: colors.text, fontSize: 16, fontWeight: '700', marginBottom: 10 },
|
||
lab: { color: colors.muted, fontSize: 12, marginBottom: 6 },
|
||
inp: {
|
||
borderWidth: 1,
|
||
borderColor: colors.border,
|
||
borderRadius: 10,
|
||
padding: 12,
|
||
color: colors.text,
|
||
backgroundColor: colors.card,
|
||
},
|
||
btn: {
|
||
backgroundColor: colors.accent,
|
||
paddingVertical: 14,
|
||
borderRadius: 12,
|
||
alignItems: 'center',
|
||
marginTop: 10,
|
||
},
|
||
btnGhost: {
|
||
backgroundColor: 'transparent',
|
||
borderWidth: 1,
|
||
borderColor: colors.border,
|
||
},
|
||
btnText: { color: '#fff', fontWeight: '700', fontSize: 16 },
|
||
btnTextGhost: { color: colors.text, fontWeight: '700', fontSize: 16 },
|
||
foot: { color: colors.muted, marginTop: 24, fontSize: 12, lineHeight: 18 },
|
||
});
|