Files
mdb/app/(tabs)/investisseurs.tsx
Bastien COIGNOUX bd325fe456 init
2026-05-03 20:18:33 +02:00

235 lines
7.3 KiB
TypeScript

import { useState } from 'react';
import {
Alert,
FlatList,
Modal,
Pressable,
StyleSheet,
Text,
View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { LabeledField } from '../../src/components/LabeledField';
import { PrimaryButton } from '../../src/components/PrimaryButton';
import { useApp } from '../../src/context/AppContext';
import { colors } from '../../src/theme/colors';
import type { InvestisseurRow } from '../../src/data/types';
import { router } from 'expo-router';
function parseNum(s: string): number | null {
const v = Number(s.replace(',', '.').replace(/\s/g, ''));
return Number.isFinite(v) ? v : null;
}
export default function InvestisseursScreen() {
const insets = useSafeAreaInsets();
const app = useApp();
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState<InvestisseurRow | null>(null);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [minMargin, setMinMargin] = useState('12');
const [maxTicket, setMaxTicket] = useState('');
const [zones, setZones] = useState('');
const cloudNeedsAuth = app.runtimeMode === 'cloud' && !app.user;
const openNew = () => {
setEditing(null);
setName('');
setEmail('');
setPhone('');
setMinMargin('12');
setMaxTicket('');
setZones('');
setOpen(true);
};
const openEdit = (row: InvestisseurRow) => {
setEditing(row);
setName(row.display_name);
setEmail(row.email ?? '');
setPhone(row.phone ?? '');
setMinMargin(String(row.min_margin_pct));
setMaxTicket(row.max_ticket_eur != null ? String(row.max_ticket_eur) : '');
setZones((row.zones ?? []).join(', '));
setOpen(true);
};
const save = async () => {
if (!app.user) {
router.push('/auth/login');
return;
}
const uid = app.user.id;
const mm = parseNum(minMargin) ?? 12;
const mt = maxTicket.trim() ? parseNum(maxTicket) : null;
const z = zones
.split(',')
.map((s) => s.trim())
.filter(Boolean);
await app.upsertInvestisseur({
id: editing?.id,
user_id: uid,
display_name: name.trim() || 'Investisseur',
email: email.trim() || null,
phone: phone.trim() || null,
min_margin_pct: mm,
max_ticket_eur: mt,
zones: z.length ? z : null,
strategies: null,
notes: null,
});
setOpen(false);
};
if (cloudNeedsAuth) {
return (
<View style={[styles.center, { paddingTop: insets.top }]}>
<Text style={styles.muted}>Connectez-vous pour gérer vos investisseurs.</Text>
<PrimaryButton title="Connexion" onPress={() => router.push('/auth/login')} />
</View>
);
}
return (
<View style={styles.root}>
<FlatList
data={app.investisseurs}
keyExtractor={(i) => i.id}
contentContainerStyle={{
padding: 16,
paddingBottom: insets.bottom + 80,
}}
ListEmptyComponent={
<Text style={styles.muted}>
Ajoutez des profils pour le module « Investisseur flash » (matching
marge / ticket / zones).
</Text>
}
renderItem={({ item }) => (
<Pressable style={styles.card} onPress={() => openEdit(item)}>
<Text style={styles.name}>{item.display_name}</Text>
<Text style={styles.meta}>
Marge mini {item.min_margin_pct}% ticket max{' '}
{item.max_ticket_eur != null
? `${item.max_ticket_eur.toLocaleString('fr-FR')}`
: '—'}
</Text>
{item.zones?.length ? (
<Text style={styles.meta}>Zones : {item.zones.join(', ')}</Text>
) : null}
</Pressable>
)}
/>
<View style={[styles.fabRow, { bottom: insets.bottom + 16 }]}>
<PrimaryButton title="Nouvel investisseur" onPress={openNew} />
</View>
<Modal visible={open} animationType="slide" transparent>
<View style={styles.modalBackdrop}>
<View style={[styles.modalCard, { paddingBottom: insets.bottom + 16 }]}>
<Text style={styles.modalTitle}>
{editing ? 'Modifier investisseur' : 'Nouvel investisseur'}
</Text>
<LabeledField label="Nom" value={name} onChangeText={setName} />
<LabeledField
label="E-mail"
autoCapitalize="none"
value={email}
onChangeText={setEmail}
/>
<LabeledField label="Téléphone" value={phone} onChangeText={setPhone} />
<LabeledField
label="Marge nette minimum (%)"
keyboardType="decimal-pad"
value={minMargin}
onChangeText={setMinMargin}
/>
<LabeledField
label="Ticket max (€) — optionnel"
keyboardType="number-pad"
value={maxTicket}
onChangeText={setMaxTicket}
/>
<LabeledField
label="Zones (ville ou CP, séparés par des virgules)"
value={zones}
onChangeText={setZones}
/>
<PrimaryButton title="Enregistrer" onPress={() => void save()} />
{editing ? (
<PrimaryButton
title="Supprimer"
variant="danger"
containerStyle={{ marginTop: 10 }}
onPress={() => {
Alert.alert(
'Supprimer',
'Confirmer la suppression ?',
[
{ text: 'Annuler', style: 'cancel' },
{
text: 'Supprimer',
style: 'destructive',
onPress: () => {
void app.deleteInvestisseur(editing.id).then(() =>
setOpen(false),
);
},
},
],
);
}}
/>
) : null}
<PrimaryButton
title="Fermer"
variant="ghost"
containerStyle={{ marginTop: 12 }}
onPress={() => setOpen(false)}
/>
</View>
</View>
</Modal>
</View>
);
}
const styles = StyleSheet.create({
root: { flex: 1, backgroundColor: colors.bg },
center: { flex: 1, padding: 20, backgroundColor: colors.bg, gap: 12 },
muted: { color: colors.textMuted, textAlign: 'center', lineHeight: 20 },
card: {
backgroundColor: colors.bgCard,
borderRadius: 14,
padding: 16,
marginBottom: 12,
borderWidth: 1,
borderColor: colors.border,
},
name: { color: colors.text, fontSize: 17, fontWeight: '700' },
meta: { color: colors.textMuted, marginTop: 6, fontSize: 13 },
fabRow: { position: 'absolute', left: 16, right: 16 },
modalBackdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.55)',
justifyContent: 'flex-end',
},
modalCard: {
backgroundColor: colors.bgCard,
borderTopLeftRadius: 18,
borderTopRightRadius: 18,
padding: 20,
borderWidth: 1,
borderColor: colors.border,
},
modalTitle: {
color: colors.text,
fontSize: 20,
fontWeight: '700',
marginBottom: 16,
},
});