init
This commit is contained in:
26
mdb-predator/app/_layout.tsx
Normal file
26
mdb-predator/app/_layout.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import 'react-native-gesture-handler';
|
||||
import { Stack } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { colors } from '../src/theme/colors';
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<StatusBar style="light" />
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: colors.card },
|
||||
headerTintColor: colors.text,
|
||||
contentStyle: { backgroundColor: colors.bg },
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="index" options={{ title: 'MDB-PREDATOR' }} />
|
||||
<Stack.Screen
|
||||
name="field"
|
||||
options={{ title: 'Field visit' }}
|
||||
/>
|
||||
</Stack>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
7
mdb-predator/app/field.tsx
Normal file
7
mdb-predator/app/field.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { FieldVisitChecklist } from '../src/components/FieldVisitChecklist';
|
||||
|
||||
const DEMO_PROPERTY_ID = 'local-demo';
|
||||
|
||||
export default function FieldScreen() {
|
||||
return <FieldVisitChecklist propertyId={DEMO_PROPERTY_ID} />;
|
||||
}
|
||||
146
mdb-predator/app/index.tsx
Normal file
146
mdb-predator/app/index.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
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 },
|
||||
});
|
||||
Reference in New Issue
Block a user