298 lines
9.0 KiB
TypeScript
298 lines
9.0 KiB
TypeScript
import { Ionicons } from '@expo/vector-icons';
|
||
import { router } from 'expo-router';
|
||
import { useEffect, useMemo, useState } from 'react';
|
||
import {
|
||
Alert,
|
||
Pressable,
|
||
SectionList,
|
||
StyleSheet,
|
||
Text,
|
||
View,
|
||
} from 'react-native';
|
||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||
import { PrimaryButton } from '../../src/components/PrimaryButton';
|
||
import { useApp } from '../../src/context/AppContext';
|
||
import type { DealSourceRow, DossierRow } from '../../src/data/types';
|
||
import {
|
||
ensureNotificationPermission,
|
||
notifyGradeADealLocal,
|
||
useDealsSourcesGradeAAlerts,
|
||
} from '../../src/hooks/useDealsSourcesGradeAAlerts';
|
||
import { colors } from '../../src/theme/colors';
|
||
|
||
type SectionRow =
|
||
| { kind: 'deal'; deal: DealSourceRow }
|
||
| { kind: 'dossier'; dossier: DossierRow };
|
||
|
||
export default function DossiersListScreen() {
|
||
const insets = useSafeAreaInsets();
|
||
const app = useApp();
|
||
const [scoutBusy, setScoutBusy] = useState(false);
|
||
|
||
const cloudNeedsAuth =
|
||
app.runtimeMode === 'cloud' && !app.user && app.supabase;
|
||
const needsSetup = app.runtimeMode === 'none';
|
||
|
||
const sortedDeals = useMemo(
|
||
() =>
|
||
[...app.dealSources].sort(
|
||
(a, b) => b.opportunity_score - a.opportunity_score,
|
||
),
|
||
[app.dealSources],
|
||
);
|
||
|
||
const sections = useMemo(() => {
|
||
const dealRows: SectionRow[] = sortedDeals.map((deal) => ({
|
||
kind: 'deal' as const,
|
||
deal,
|
||
}));
|
||
const dossierRows: SectionRow[] = app.dossiers.map((dossier) => ({
|
||
kind: 'dossier' as const,
|
||
dossier,
|
||
}));
|
||
return [
|
||
{ title: 'Flux opportunités (Scout)', data: dealRows },
|
||
{ title: 'Mes dossiers', data: dossierRows },
|
||
];
|
||
}, [sortedDeals, app.dossiers]);
|
||
|
||
useDealsSourcesGradeAAlerts(
|
||
app.supabase,
|
||
app.user?.id,
|
||
app.runtimeMode === 'cloud' && !!app.user && !!app.supabase,
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (app.runtimeMode === 'cloud' && app.user) {
|
||
void ensureNotificationPermission();
|
||
}
|
||
}, [app.runtimeMode, app.user]);
|
||
|
||
return (
|
||
<View style={[styles.root, { paddingTop: 8 }]}>
|
||
{needsSetup ? (
|
||
<View style={styles.banner}>
|
||
<Text style={styles.bannerText}>
|
||
Choisissez le mode hors-ligne sur l’accueil, ou configurez Supabase
|
||
dans Réglages.
|
||
</Text>
|
||
<PrimaryButton
|
||
title="Retour accueil"
|
||
onPress={() => router.replace('/')}
|
||
/>
|
||
</View>
|
||
) : null}
|
||
{cloudNeedsAuth ? (
|
||
<View style={styles.banner}>
|
||
<Text style={styles.bannerText}>
|
||
Connectez-vous pour charger vos dossiers Supabase.
|
||
</Text>
|
||
<PrimaryButton title="Connexion" onPress={() => router.push('/auth/login')} />
|
||
</View>
|
||
) : null}
|
||
|
||
<SectionList
|
||
sections={sections}
|
||
keyExtractor={(item, index) =>
|
||
item.kind === 'deal' ? `deal-${item.deal.id}` : `dossier-${item.dossier.id}-${index}`
|
||
}
|
||
stickySectionHeadersEnabled={false}
|
||
contentContainerStyle={{
|
||
paddingHorizontal: 16,
|
||
paddingBottom: insets.bottom + 100,
|
||
}}
|
||
ListHeaderComponent={
|
||
<View style={{ marginBottom: 12 }}>
|
||
<PrimaryButton
|
||
title={scoutBusy ? 'Scout…' : 'Simuler ingest Scout (JSON)'}
|
||
loading={scoutBusy}
|
||
onPress={async () => {
|
||
if (!app.user) {
|
||
router.push('/auth/login');
|
||
return;
|
||
}
|
||
setScoutBusy(true);
|
||
const r = await app.runScoutSampleBatch();
|
||
setScoutBusy(false);
|
||
if ('error' in r) {
|
||
Alert.alert('Scout', r.error);
|
||
return;
|
||
}
|
||
if (app.runtimeMode === 'local' && r.gradeA > 0) {
|
||
notifyGradeADealLocal(
|
||
`${r.gradeA} opportunité(s) Grade A (Scout simulé)`,
|
||
);
|
||
}
|
||
Alert.alert(
|
||
'Scout',
|
||
`Insérés : ${r.inserted} — Grade A : ${r.gradeA}.`,
|
||
);
|
||
}}
|
||
/>
|
||
<Text style={styles.hint}>
|
||
Filtre : mots-clés succession / urgent / travaux important + prix/m²
|
||
sous moyenne simulée (3500 €/m²). Cloud : RPC `scout_process_batch`
|
||
après migration SQL.
|
||
</Text>
|
||
</View>
|
||
}
|
||
renderSectionHeader={({ section: { title, data } }) => (
|
||
<Text style={styles.sectionTitle}>
|
||
{title}
|
||
{title.startsWith('Flux') ? ` (${data.length})` : ` (${data.length})`}
|
||
</Text>
|
||
)}
|
||
renderItem={({ item }) =>
|
||
item.kind === 'deal' ? (
|
||
<DealSourceCard row={item.deal} />
|
||
) : (
|
||
<DossierRowCard row={item.dossier} />
|
||
)
|
||
}
|
||
ListEmptyComponent={null}
|
||
/>
|
||
|
||
<View style={[styles.fabWrap, { bottom: insets.bottom + 20 }]}>
|
||
<Pressable
|
||
accessibilityRole="button"
|
||
style={styles.fab}
|
||
onPress={async () => {
|
||
if (app.runtimeMode === 'none') {
|
||
router.replace('/');
|
||
return;
|
||
}
|
||
if (!app.user && app.runtimeMode === 'cloud') {
|
||
router.push('/auth/login');
|
||
return;
|
||
}
|
||
const id = await app.createDossier();
|
||
if (id) router.push(`/dossier/${id}`);
|
||
}}
|
||
>
|
||
<Ionicons name="add" size={32} color="#fff" />
|
||
</Pressable>
|
||
</View>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
function DealSourceCard({ row }: { row: DealSourceRow }) {
|
||
const pm = Math.round(row.price_per_m2_eur);
|
||
const dotStyle =
|
||
row.grade === 'A' ? styles.badgeA : row.grade === 'B' ? styles.badgeB : styles.badgeC;
|
||
return (
|
||
<View style={styles.dealCard}>
|
||
<View style={styles.dealHead}>
|
||
<Text style={styles.badgeText}>Grade {row.grade}</Text>
|
||
<View style={[styles.badgeDot, dotStyle]} />
|
||
<Text style={styles.score}>{row.opportunity_score.toFixed(0)} pts</Text>
|
||
</View>
|
||
<Text style={styles.cardTitle}>{row.title}</Text>
|
||
<Text style={styles.cardSub}>
|
||
{row.price_eur != null
|
||
? `${row.price_eur.toLocaleString('fr-FR')} € · ${row.surface_m2} m² · ${pm} €/m²`
|
||
: `${row.surface_m2} m²`}
|
||
</Text>
|
||
{row.distress_keywords?.length ? (
|
||
<Text style={[styles.kw, { color: colors.flash }]}>
|
||
Mots-clés : {row.distress_keywords.join(', ')}
|
||
</Text>
|
||
) : null}
|
||
{row.source_name ? (
|
||
<Text style={styles.cardMeta}>Source : {row.source_name}</Text>
|
||
) : null}
|
||
</View>
|
||
);
|
||
}
|
||
|
||
function DossierRowCard({ row }: { row: DossierRow }) {
|
||
const city = [row.postal_code, row.city].filter(Boolean).join(' ');
|
||
return (
|
||
<Pressable
|
||
style={styles.card}
|
||
onPress={() => router.push(`/dossier/${row.id}`)}
|
||
>
|
||
<Text style={styles.cardTitle}>{row.title}</Text>
|
||
{city ? <Text style={styles.cardSub}>{city}</Text> : null}
|
||
<Text style={styles.cardMeta}>Statut : {row.status}</Text>
|
||
</Pressable>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
root: { flex: 1, backgroundColor: colors.bg },
|
||
banner: {
|
||
marginHorizontal: 16,
|
||
marginBottom: 12,
|
||
padding: 14,
|
||
borderRadius: 12,
|
||
backgroundColor: colors.bgCard,
|
||
borderWidth: 1,
|
||
borderColor: colors.border,
|
||
gap: 10,
|
||
},
|
||
bannerText: { color: colors.text, lineHeight: 20 },
|
||
hint: {
|
||
color: colors.textMuted,
|
||
fontSize: 12,
|
||
lineHeight: 17,
|
||
marginTop: 10,
|
||
},
|
||
sectionTitle: {
|
||
color: colors.text,
|
||
fontSize: 15,
|
||
fontWeight: '800',
|
||
marginTop: 8,
|
||
marginBottom: 8,
|
||
},
|
||
dealCard: {
|
||
backgroundColor: '#121a24',
|
||
borderRadius: 14,
|
||
padding: 16,
|
||
marginBottom: 12,
|
||
borderWidth: 1,
|
||
borderColor: colors.border,
|
||
},
|
||
dealHead: { flexDirection: 'row', alignItems: 'center', marginBottom: 8, gap: 8 },
|
||
badgeDot: { width: 8, height: 8, borderRadius: 4 },
|
||
badgeA: { backgroundColor: '#3fb950' },
|
||
badgeB: { backgroundColor: '#d29922' },
|
||
badgeC: { backgroundColor: colors.textMuted },
|
||
badgeText: {
|
||
color: colors.text,
|
||
fontWeight: '900',
|
||
fontSize: 13,
|
||
marginRight: 4,
|
||
},
|
||
score: { color: colors.textMuted, fontSize: 12, marginLeft: 'auto' },
|
||
kw: { color: colors.flash ?? '#7ee787', marginTop: 6, fontSize: 12 },
|
||
card: {
|
||
backgroundColor: colors.bgCard,
|
||
borderRadius: 14,
|
||
padding: 16,
|
||
marginBottom: 12,
|
||
borderWidth: 1,
|
||
borderColor: colors.border,
|
||
},
|
||
cardTitle: { color: colors.text, fontSize: 17, fontWeight: '700' },
|
||
cardSub: { color: colors.textMuted, marginTop: 4 },
|
||
cardMeta: { color: colors.textMuted, marginTop: 8, fontSize: 12 },
|
||
fabWrap: {
|
||
position: 'absolute',
|
||
right: 20,
|
||
},
|
||
fab: {
|
||
width: 58,
|
||
height: 58,
|
||
borderRadius: 29,
|
||
backgroundColor: colors.accent,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
elevation: 4,
|
||
shadowColor: '#000',
|
||
shadowOpacity: 0.25,
|
||
shadowRadius: 6,
|
||
shadowOffset: { width: 0, height: 2 },
|
||
},
|
||
});
|