-import { View, Text, StyleSheet, ScrollView } from "react-native";
-import { useState, useCallback, useEffect } from "react";
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ useWindowDimensions,
+} from "react-native";
+import { useState, useCallback, useEffect, useMemo } from "react";
import { SafeAreaView } from "react-native-safe-area-context";
import { useFocusEffect, useNavigation } from "@react-navigation/native";
import { loadSettings } from "../utils/settingsStorage";
import type { UserSettings } from "../models/UserSettings";
-import MarketCard from "../components/MarketCard";
-import StrategyCard from "../components/StrategyCard";
-import WalletCard from "../components/WalletCard";
-import ActionsCard from "../components/ActionsCard";
-
import { ui } from "../components/ui/uiStyles";
-// ✅ Socket.IO
import { socketService } from "../services/socketService";
import { SERVER_URL } from "../config/env";
import type { Alert } from "../types/Alert";
-
-// ✅ Store global alertes
import { alertStore } from "../services/alertStore";
-
-// ✅ Notifications locales Expo
import { showAlertNotification } from "../services/notificationService";
/**
- * DashboardScreen
- * ----------------
- * Écran principal mobile.
- * - Charge Dashboard + Settings
- * - Refresh auto si activé
- * - Socket.IO non bloquant pour alertes live
- * - Dashboard = résumé : on affiche UNE seule alerte (la dernière)
- *
- * Notifications :
- * - Si settings.notificationsEnabled = true -> notification locale à chaque alerte reçue
+ * DashboardScreen (WF-01) — Responsive
+ * -----------------------------------
+ * Objectif : coller au mockup, éviter le scroll.
+ * - On enlève le gros bloc "Actions" (navigation via header icons)
+ * - Certaines cartes deviennent cliquables (tap-to-open)
*/
export default function DashboardScreen() {
+ const { height } = useWindowDimensions();
+ const compact = height < 760;
+
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [settings, setSettings] = useState<UserSettings | null>(null);
- const [loading, setLoading] = useState<boolean>(true);
- // erreurs REST/refresh
+ const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
- // Socket state (non bloquant)
const [socketConnected, setSocketConnected] = useState<boolean>(false);
const [socketError, setSocketError] = useState<string | null>(null);
- // Résumé dashboard (dernière alerte reçue affichée)
const [liveAlerts, setLiveAlerts] = useState<Alert[]>([]);
+ const [lastRefreshMs, setLastRefreshMs] = useState<number | null>(null);
+ const [refreshing, setRefreshing] = useState<boolean>(false);
+
const navigation = useNavigation();
- /**
- * Chargement initial + rechargement quand on revient sur l'écran
- */
useFocusEffect(
useCallback(() => {
let isActive = true;
loadSettings(),
]);
- if (isActive) {
- setSummary(dashboardData);
- setSettings(userSettings);
- }
+ if (!isActive) return;
+
+ setSummary(dashboardData);
+ setSettings(userSettings);
+ setLastRefreshMs(Date.now());
} catch {
if (isActive) setError("Impossible de charger le dashboard.");
} finally {
}, [])
);
- /**
- * Refresh auto (sans clignotement global)
- */
useEffect(() => {
if (!settings) return;
if (settings.refreshMode !== "auto") return;
const intervalId = setInterval(async () => {
try {
const data = await fetchDashboardSummary();
- if (!cancelled) setSummary(data);
+ if (!cancelled) {
+ setSummary(data);
+ setLastRefreshMs(Date.now());
+ }
} catch {
if (!cancelled) setError("Erreur lors du rafraîchissement automatique.");
}
};
}, [settings]);
- /**
- * Socket.IO (non bloquant)
- * - En Step 1/2/3 on utilise un userId de test (auth pas encore là)
- * - Si serveur KO, on n'empêche pas l'app de fonctionner
- */
+ const handleManualRefresh = async () => {
+ try {
+ setRefreshing(true);
+ const data = await fetchDashboardSummary();
+ setSummary(data);
+ setLastRefreshMs(Date.now());
+ setError(null);
+ } catch {
+ setError("Erreur lors de l'actualisation.");
+ } finally {
+ setRefreshing(false);
+ }
+ };
+
useEffect(() => {
if (!settings) return;
setSocketError(null);
- // ✅ userId de test pour avancer maintenant
const userId = "test-user";
try {
}
const unsubscribeAlert = socketService.onAlert((alert) => {
- // 1) Stock global (écran Alertes)
alertStore.add(alert);
+ setLiveAlerts((prev) => [alert, ...prev].slice(0, 100));
- // 2) Résumé dashboard (dernière alerte)
- setLiveAlerts((prev) => [alert, ...prev].slice(0, 50));
-
- // 3) Notification locale (si activée)
if (settings.notificationsEnabled) {
- // On encapsule pour ne jamais casser le thread UI
void (async () => {
try {
await showAlertNotification(alert);
};
}, [settings]);
- // Chargement
+ const urgentAlert: Alert | null = useMemo(() => {
+ if (liveAlerts.length === 0) return null;
+
+ const critical = liveAlerts.filter((a) => a.alertLevel === "CRITICAL");
+ if (critical.length > 0) return critical[0];
+
+ const warning = liveAlerts.filter((a) => a.alertLevel === "WARNING");
+ if (warning.length > 0) return warning[0];
+
+ return liveAlerts[0];
+ }, [liveAlerts]);
+
if (loading) {
return (
<View style={ui.centered}>
);
}
- // Erreur bloquante (si rien à afficher)
if (error && (!summary || !settings)) {
return (
<View style={ui.centered}>
);
}
- // Sécurité
if (!summary || !settings) {
return (
<View style={ui.centered}>
return (
<SafeAreaView style={styles.safeArea}>
- <ScrollView contentContainerStyle={ui.container}>
- {/* Bannière warning REST/refresh (non bloquant) */}
+ <ScrollView
+ contentContainerStyle={[ui.container, compact && styles.containerCompact]}
+ showsVerticalScrollIndicator={false}
+ >
{error && (
- <View style={[ui.banner, styles.bannerWarning]}>
- <Text style={[ui.bannerText, styles.bannerWarningText]}>{error}</Text>
+ <View style={[ui.banner, styles.bannerWarning, compact && styles.bannerCompact]}>
+ <Text style={[ui.bannerText, styles.bannerWarningText]} numberOfLines={2}>
+ {error}
+ </Text>
</View>
)}
- {/* Bannière Socket (non bloquant) */}
- <View style={[ui.banner, styles.bannerSocket]}>
- <Text style={[ui.bannerText, styles.socketTitle]}>
- Socket.IO : {socketConnected ? "connecté ✅" : "déconnecté ⚠️"}
+ {/* 1) CONSEILLER */}
+ <View style={[ui.card, compact && styles.cardCompact]}>
+ <Text style={[ui.title, compact && styles.titleCompact]}>Conseiller</Text>
+
+ <Text style={[ui.bigCenter, compact && styles.bigCompact]}>{summary.decision}</Text>
+
+ <Text style={[ui.muted, styles.centerText]} numberOfLines={compact ? 2 : 3}>
+ Pourquoi ? {summary.reason}
</Text>
- {!!socketError && <Text style={ui.muted}>{socketError}</Text>}
- <Text style={[ui.muted, { marginTop: 4 }]}>
- Alertes reçues : {liveAlerts.length}
+ <Text style={[ui.muted, { marginTop: compact ? 6 : 10 }]} numberOfLines={1}>
+ Stratégie : <Text style={styles.boldInline}>{settings.selectedStrategyKey}</Text>
</Text>
+
+ <TouchableOpacity
+ style={[ui.button, styles.fullButton, compact && styles.buttonCompact]}
+ onPress={() => navigation.navigate("Strategy" as never)}
+ >
+ <Text style={ui.buttonText}>Sélectionner stratégie</Text>
+ </TouchableOpacity>
</View>
- {/* Dashboard : afficher uniquement la DERNIÈRE alerte */}
- {liveAlerts.length > 0 && (
- <View style={styles.alertItem}>
- <Text style={styles.alertHeader}>
- {liveAlerts[0].alertLevel} — {liveAlerts[0].action}{" "}
- {liveAlerts[0].pair}
+ {/* 2) PORTEFEUILLE (pas encore cliquable car pas d’écran Wallet dédié) */}
+ <View style={[ui.card, compact && styles.cardCompact]}>
+ <Text style={[ui.title, compact && styles.titleCompact]}>Portefeuille</Text>
+
+ <View style={ui.rowBetween}>
+ <Text style={ui.value}>Valeur Totale :</Text>
+ <Text style={ui.valueBold}>10 000 {settings.currency}</Text>
+ </View>
+
+ {!compact && (
+ <Text style={[ui.muted, { marginTop: 6 }]}>
+ Step 1 : mono-utilisateur / mono-crypto
</Text>
- <Text style={styles.alertBody}>
- {(liveAlerts[0].confidence * 100).toFixed(0)}% —{" "}
- {liveAlerts[0].reason}
+ )}
+ </View>
+
+ {/* 3) URGENCE — cliquable => Alertes */}
+ <TouchableOpacity
+ activeOpacity={0.85}
+ onPress={() => navigation.navigate("Alerts" as never)}
+ >
+ <View style={[ui.card, compact && styles.cardCompact]}>
+ <Text style={[ui.title, compact && styles.titleCompact]}>Urgence</Text>
+
+ {urgentAlert ? (
+ <View style={[styles.urgentBox, compact && styles.urgentBoxCompact]}>
+ <Text style={styles.urgentTitle} numberOfLines={2}>
+ {urgentAlert.alertLevel} : {urgentAlert.reason}
+ </Text>
+ <Text style={ui.muted} numberOfLines={1}>
+ {urgentAlert.action} {urgentAlert.pair} —{" "}
+ {(urgentAlert.confidence * 100).toFixed(0)}%
+ </Text>
+ </View>
+ ) : (
+ <Text style={ui.muted}>Aucune alerte pour le moment.</Text>
+ )}
+
+ <Text style={[ui.muted, { marginTop: compact ? 6 : 8 }]} numberOfLines={1}>
+ Socket : {socketConnected ? "connecté ✅" : "déconnecté ⚠️"}
+ {socketError ? ` — ${socketError}` : ""}
</Text>
- <Text style={styles.alertHint}>
- Voir toutes les alertes dans “Alertes”.
+
+ <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={1}>
+ Appuie pour ouvrir “Alertes”
</Text>
</View>
- )}
+ </TouchableOpacity>
+
+ {/* 4) PRIX BTC — cliquable => Historique */}
+ <TouchableOpacity
+ activeOpacity={0.85}
+ onPress={() => navigation.navigate("History" as never)}
+ >
+ <View style={[ui.card, compact && styles.cardCompact]}>
+ <View style={styles.priceHeaderRow}>
+ <Text style={[ui.title, compact && styles.titleCompact]}>Prix BTC</Text>
+
+ <TouchableOpacity
+ style={[
+ styles.refreshBtn,
+ refreshing && styles.refreshBtnDisabled,
+ compact && styles.refreshBtnCompact,
+ ]}
+ onPress={handleManualRefresh}
+ disabled={refreshing}
+ >
+ <Text style={styles.refreshBtnText}>{refreshing ? "…" : "Actualiser"}</Text>
+ </TouchableOpacity>
+ </View>
+
+ <Text style={ui.muted} numberOfLines={1}>
+ Dernière maj :{" "}
+ {lastRefreshMs ? new Date(lastRefreshMs).toLocaleTimeString() : "—"}
+ </Text>
- <MarketCard summary={summary} settings={settings} />
-
- <StrategyCard
- summary={summary}
- settings={settings}
- onGoStrategy={() => navigation.navigate("Strategy" as never)}
- />
-
- <WalletCard settings={settings} />
-
- <ActionsCard
- onGoSettings={() => navigation.navigate("Settings" as never)}
- onGoHistory={() => navigation.navigate("History" as never)}
- onGoAlerts={() => navigation.navigate("Alerts" as never)}
- />
+ <View style={[styles.priceCard, compact && styles.priceCardCompact]}>
+ <View style={ui.rowBetween}>
+ <Text style={ui.value}>Prix BTC</Text>
+ <Text style={styles.priceBig}>
+ {summary.price.toFixed(2)} {settings.currency}
+ </Text>
+ </View>
+ </View>
+
+ <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={1}>
+ Appuie pour ouvrir “Historique”
+ </Text>
+ </View>
+ </TouchableOpacity>
</ScrollView>
</SafeAreaView>
);
backgroundColor: ui.screen.backgroundColor,
},
- errorText: {
- color: "#dc2626",
- fontWeight: "900",
+ containerCompact: {
+ padding: 12,
+ },
+
+ cardCompact: {
+ padding: 12,
+ marginBottom: 10,
+ },
+
+ titleCompact: {
+ marginBottom: 6,
+ },
+
+ bigCompact: {
+ fontSize: 24,
+ marginVertical: 4,
},
bannerWarning: {
bannerWarningText: {
color: "#ca8a04",
},
+ bannerCompact: {
+ padding: 10,
+ marginBottom: 10,
+ },
- bannerSocket: {
- borderColor: "#cbd5e1",
+ errorText: {
+ color: "#dc2626",
+ fontWeight: "900",
},
- socketTitle: {
+
+ centerText: {
+ textAlign: "center",
+ },
+
+ boldInline: {
+ fontWeight: "900",
color: "#0f172a",
},
- // Dernière alerte affichée (résumé)
- alertItem: {
+ fullButton: {
+ flexGrow: 0,
+ flexBasis: "auto",
+ width: "100%",
+ marginTop: 12,
+ },
+ buttonCompact: {
+ paddingVertical: 10,
+ marginTop: 10,
+ },
+
+ urgentBox: {
borderWidth: 1,
borderColor: "#e5e7eb",
- padding: 10,
borderRadius: 10,
- marginBottom: 12,
+ padding: 10,
backgroundColor: "#ffffff",
},
- alertHeader: {
+ urgentBoxCompact: {
+ padding: 8,
+ },
+ urgentTitle: {
fontWeight: "900",
color: "#0f172a",
},
- alertBody: {
- marginTop: 4,
- color: "#334155",
+
+ priceHeaderRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginBottom: 8,
+ },
+ refreshBtn: {
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ borderRadius: 10,
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
+ backgroundColor: "#fff",
+ },
+ refreshBtnCompact: {
+ paddingVertical: 6,
+ },
+ refreshBtnDisabled: {
+ opacity: 0.6,
+ },
+ refreshBtnText: {
+ fontWeight: "900",
+ color: "#0f172a",
},
- alertHint: {
- marginTop: 6,
- fontSize: 12,
- opacity: 0.7,
+
+ priceCard: {
+ marginTop: 10,
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
+ borderRadius: 10,
+ padding: 12,
+ backgroundColor: "#ffffff",
+ },
+ priceCardCompact: {
+ paddingVertical: 10,
+ },
+ priceBig: {
+ fontSize: 22,
+ fontWeight: "900",
+ color: "#0f172a",
},
});
\ No newline at end of file