235 lines
7.3 KiB
TypeScript
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,
|
|
},
|
|
});
|