]> git.digitality.be Git - pdw25-26/commitdiff
Mobile : Modification + AlertsScreen created
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Wed, 25 Feb 2026 12:10:56 +0000 (13:10 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Wed, 25 Feb 2026 12:10:56 +0000 (13:10 +0100)
Wallette/mobile/App.tsx
Wallette/mobile/src/components/ActionsCard.tsx
Wallette/mobile/src/screens/AlertsScreen.tsx [new file with mode: 0644]
Wallette/mobile/src/screens/DashboardScreen.tsx
Wallette/mobile/src/services/alertStore.ts [new file with mode: 0644]

index e491b2b9915dfb303d3b0153f767ddc6300c5872..34984601c94f792372c86433e9bb803a227751a7 100644 (file)
@@ -4,12 +4,15 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack";
 import DashboardScreen from "./src/screens/DashboardScreen";
 import SettingsScreen from "./src/screens/SettingsScreen";
 import HistoryScreen from "./src/screens/HistoryScreen";
+import AlertsScreen from "./src/screens/AlertsScreen";
+
 
 // Types des routes (pour éviter les erreurs de navigation)
 export type RootStackParamList = {
   Dashboard: undefined;
   Settings: undefined;
   History: undefined;
+  Alerts: undefined;
 };
 
 const Stack = createNativeStackNavigator<RootStackParamList>();
@@ -35,6 +38,11 @@ export default function App() {
         component={HistoryScreen} 
         options={{ title: "Historique" }} 
         />
+
+        <Stack.Screen 
+        name="Alerts" 
+        component={AlertsScreen} 
+        options={{ title: "Alertes" }} />
         
       </Stack.Navigator>
     </NavigationContainer>
index dfc2a6c428fe5276ec3dff390476430743a8d084..d49b260dd025f9497ac7aae0f06cb2f3615a8154 100644 (file)
@@ -4,6 +4,7 @@ import { ui } from "./ui/uiStyles";
 type Props = {
   onGoSettings: () => void;
   onGoHistory: () => void;
+  onGoAlerts: () => void;
 };
 
 /**
@@ -14,15 +15,16 @@ type Props = {
  * - Voir stratégie (placeholder)
  * - Historique (placeholder)
  * - Paramètres (navigation réelle via callback)
+ * - Alertes (navigation réelle via callback)
  */
-export default function ActionsCard({ onGoSettings, onGoHistory }: Props) {
+export default function ActionsCard({ onGoSettings, onGoHistory, onGoAlerts }: Props) {
   return (
     <View style={ui.card}>
       <Text style={ui.title}>Actions</Text>
 
       <View style={[ui.rowBetween, { flexWrap: "wrap", gap: 8 }]}>
-        <TouchableOpacity style={ui.button} onPress={() => {}}>
-          <Text style={ui.buttonText}>Voir stratégie</Text>
+        <TouchableOpacity style={ui.button} onPress={onGoAlerts}>
+          <Text style={ui.buttonText}>Alertes</Text>
         </TouchableOpacity>
 
         <TouchableOpacity style={ui.button} onPress={onGoHistory}>
diff --git a/Wallette/mobile/src/screens/AlertsScreen.tsx b/Wallette/mobile/src/screens/AlertsScreen.tsx
new file mode 100644 (file)
index 0000000..4333d5d
--- /dev/null
@@ -0,0 +1,140 @@
+import { View, Text, StyleSheet, FlatList } from "react-native";
+import { useEffect, useMemo, useState } from "react";
+import { SafeAreaView } from "react-native-safe-area-context";
+
+import type { Alert } from "../types/Alert";
+import { alertStore } from "../services/alertStore";
+import { ui } from "../components/ui/uiStyles";
+
+// Types locaux pour le tri/couleurs (évite dépendre d'exports manquants)
+type AlertLevel = "CRITICAL" | "WARNING" | "INFO";
+type TradeDecision = "BUY" | "SELL" | "HOLD" | "STOP_LOSS";
+
+function levelRank(level: AlertLevel) {
+  switch (level) {
+    case "CRITICAL":
+      return 3;
+    case "WARNING":
+      return 2;
+    case "INFO":
+    default:
+      return 1;
+  }
+}
+
+function getLevelColor(level: AlertLevel): string {
+  switch (level) {
+    case "CRITICAL":
+      return "#dc2626";
+    case "WARNING":
+      return "#ca8a04";
+    case "INFO":
+    default:
+      return "#2563eb";
+  }
+}
+
+function getActionColor(action: TradeDecision): string {
+  switch (action) {
+    case "BUY":
+      return "#16a34a";
+    case "SELL":
+      return "#dc2626";
+    case "STOP_LOSS":
+      return "#991b1b";
+    case "HOLD":
+    default:
+      return "#ca8a04";
+  }
+}
+
+export default function AlertsScreen() {
+  const [alerts, setAlerts] = useState<Alert[]>([]);
+
+  useEffect(() => {
+    const unsub = alertStore.subscribe(setAlerts);
+    return () => {
+      unsub();
+    };
+  }, []);
+
+  const sorted = useMemo(() => {
+    return [...alerts].sort((a, b) => {
+      // cast car ton type Alert peut ne pas déclarer explicitement les unions
+      const la = a.alertLevel as AlertLevel;
+      const lb = b.alertLevel as AlertLevel;
+
+      const byLevel = levelRank(lb) - levelRank(la);
+      if (byLevel !== 0) return byLevel;
+
+      const ta = a.timestamp ?? 0;
+      const tb = b.timestamp ?? 0;
+      return tb - ta;
+    });
+  }, [alerts]);
+
+  return (
+    <SafeAreaView style={styles.safeArea}>
+      <FlatList
+        contentContainerStyle={ui.container}
+        data={sorted}
+        keyExtractor={(_, idx) => `alert-${idx}`}
+        ListEmptyComponent={
+          <View style={ui.card}>
+            <Text style={ui.title}>Aucune alerte</Text>
+            <Text style={ui.muted}>En attente d’alertes Socket.IO…</Text>
+          </View>
+        }
+        renderItem={({ item }) => {
+          const lvl = item.alertLevel as AlertLevel;
+          const act = item.action as TradeDecision;
+
+          const lvlColor = getLevelColor(lvl);
+          const actColor = getActionColor(act);
+
+          return (
+            <View style={ui.card}>
+              <View style={ui.rowBetween}>
+                <Text style={ui.valueBold}>{item.pair}</Text>
+                <Text style={ui.muted}>
+                  {item.timestamp ? new Date(item.timestamp).toLocaleString() : "—"}
+                </Text>
+              </View>
+
+              <View style={{ flexDirection: "row", gap: 8, flexWrap: "wrap", marginTop: 10 }}>
+                <View style={[ui.badge, { backgroundColor: `${lvlColor}22`, marginTop: 0 }]}>
+                  <Text style={[ui.badgeText, { color: lvlColor }]}>{lvl}</Text>
+                </View>
+
+                <View style={[ui.badge, { backgroundColor: `${actColor}22`, marginTop: 0 }]}>
+                  <Text style={[ui.badgeText, { color: actColor }]}>{act}</Text>
+                </View>
+              </View>
+
+              <View style={[ui.rowBetween, { marginTop: 10 }]}>
+                <Text style={ui.value}>Confiance</Text>
+                <Text style={ui.valueBold}>{(item.confidence * 100).toFixed(0)}%</Text>
+              </View>
+
+              {typeof item.price === "number" && (
+                <View style={[ui.rowBetween, { marginTop: 6 }]}>
+                  <Text style={ui.value}>Prix</Text>
+                  <Text style={ui.valueBold}>{item.price.toFixed(2)}</Text>
+                </View>
+              )}
+
+              <Text style={[ui.muted, { marginTop: 10 }]}>{item.reason}</Text>
+            </View>
+          );
+        }}
+      />
+    </SafeAreaView>
+  );
+}
+
+const styles = StyleSheet.create({
+  safeArea: {
+    flex: 1,
+    backgroundColor: ui.screen.backgroundColor,
+  },
+});
\ No newline at end of file
index a8a34b03a023a47ec806fde1ab5c8dd5ff1b3ee7..01da368746fef7e0264c43d9cb665ff8acfed3c6 100644 (file)
@@ -7,6 +7,7 @@ import type { DashboardSummary } from "../types/DashboardSummary";
 import { fetchDashboardSummary } from "../services/dashboardService";
 import { loadSettings } from "../utils/settingsStorage";
 import type { UserSettings } from "../models/UserSettings";
+import { alertStore } from "../services/alertStore";
 
 import MarketCard from "../components/MarketCard";
 import StrategyCard from "../components/StrategyCard";
@@ -26,8 +27,13 @@ import type { Alert } from "../types/Alert";
  * Écran principal mobile.
  * - Charge Dashboard + Settings
  * - Refresh auto si activé
- * - Socket.IO optionnel (non bloquant) pour alertes live
- * - UI cohérente via uiStyles/theme
+ * - Socket.IO non bloquant pour alertes live
+ * - Dashboard = résumé : on affiche UNE seule alerte (la dernière)
+ *
+ *
+ * Important :
+ * - Le Socket est NON BLOQUANT : si le serveur n'est pas dispo, l'app continue de marcher.
+ * - En Step 1/2/3, on n'a pas forcément de vrai userId => on utilise un userId de test.
  */
 export default function DashboardScreen() {
   const [summary, setSummary] = useState<DashboardSummary | null>(null);
@@ -106,22 +112,16 @@ export default function DashboardScreen() {
 
   /**
    * Socket.IO (non bloquant)
-   * - Si userId absent => on désactive socket proprement.
-   * - Si serveur pas prêt => connect_error timeout => on affiche un message, sans crasher.
+   * - 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
    */
   useEffect(() => {
     if (!settings) return;
 
     setSocketError(null);
 
-    // ✅ userId optionnel pour Step 1/2/3 (pas d'auth)
-    const userId = settings.userId;
-
-    if (!userId) {
-      setSocketConnected(false);
-      setSocketError("Socket désactivé : userId absent.");
-      return;
-    }
+    // ✅ userId de test pour avancer maintenant
+    const userId = "test-user";
 
     try {
       socketService.connect(SERVER_URL, userId);
@@ -133,7 +133,8 @@ export default function DashboardScreen() {
     }
 
     const unsubscribeAlert = socketService.onAlert((alert) => {
-      setLiveAlerts((prev) => [alert, ...prev].slice(0, 50));
+      alertStore.add(alert); // ✅ stock global
+      setLiveAlerts((prev) => [alert, ...prev].slice(0, 50)); // (optionnel) résumé dashboard
     });
 
     socketService.ping();
@@ -194,6 +195,23 @@ export default function DashboardScreen() {
           </Text>
         </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}
+            </Text>
+            <Text style={styles.alertBody}>
+              {(liveAlerts[0].confidence * 100).toFixed(0)}% —{" "}
+              {liveAlerts[0].reason}
+            </Text>
+            <Text style={styles.alertHint}>
+              Voir toutes les alertes dans “Alertes”.
+            </Text>
+          </View>
+        )}
+
         <MarketCard summary={summary} settings={settings} />
         <StrategyCard summary={summary} />
         <WalletCard settings={settings} />
@@ -201,6 +219,7 @@ export default function DashboardScreen() {
         <ActionsCard
           onGoSettings={() => navigation.navigate("Settings" as never)}
           onGoHistory={() => navigation.navigate("History" as never)}
+          onGoAlerts={() => navigation.navigate("Alerts" as never)}
         />
       </ScrollView>
     </SafeAreaView>
@@ -231,4 +250,27 @@ const styles = StyleSheet.create({
   socketTitle: {
     color: "#0f172a",
   },
+
+  // Dernière alerte affichée (résumé)
+  alertItem: {
+    borderWidth: 1,
+    borderColor: "#e5e7eb",
+    padding: 10,
+    borderRadius: 10,
+    marginBottom: 12,
+    backgroundColor: "#ffffff",
+  },
+  alertHeader: {
+    fontWeight: "900",
+    color: "#0f172a",
+  },
+  alertBody: {
+    marginTop: 4,
+    color: "#334155",
+  },
+  alertHint: {
+    marginTop: 6,
+    fontSize: 12,
+    opacity: 0.7,
+  },
 });
\ No newline at end of file
diff --git a/Wallette/mobile/src/services/alertStore.ts b/Wallette/mobile/src/services/alertStore.ts
new file mode 100644 (file)
index 0000000..4ac745d
--- /dev/null
@@ -0,0 +1,38 @@
+import type { Alert } from "../types/Alert";
+
+type Listener = (alerts: Alert[]) => void;
+
+class AlertStore {
+  private alerts: Alert[] = [];
+  private listeners = new Set<Listener>();
+
+  add(alert: Alert) {
+    this.alerts = [alert, ...this.alerts].slice(0, 200);
+    this.emit();
+  }
+
+  getAll() {
+    return this.alerts;
+  }
+
+  subscribe(listener: Listener) {
+    this.listeners.add(listener);
+    listener(this.alerts);
+
+    // ✅ cleanup doit retourner void (pas boolean)
+    return () => {
+      this.listeners.delete(listener);
+    };
+  }
+
+  clear() {
+    this.alerts = [];
+    this.emit();
+  }
+
+  private emit() {
+    for (const l of this.listeners) l(this.alerts);
+  }
+}
+
+export const alertStore = new AlertStore();
\ No newline at end of file