import streamlit as st import pandas as pd import pydeck as pdk from geopy.geocoders import Nominatim from geopy.exc import GeocoderTimedOut import time import requests import datetime # --- CONSTANTES --- COLOR_BLUE_GOTHAM = "#25C1F7" COLOR_GREEN_ZONE = "#54BD4B" COLOR_YELLOW_LINE = "#FFD700" # === CSS SPÉCIFIQUE MAP DASHBOARD (Styles additionnels seulement) === def apply_map_dashboard_styles(): st.markdown(""" """, unsafe_allow_html=True) # --- FONCTION GÉOCODAGE --- @st.cache_data(show_spinner=False, ttl=3600) def geocode_addresses(df_clients): """Géocodage via LocationIQ""" if 'Adresse' not in df_clients.columns or 'ID_Client' not in df_clients.columns: return pd.DataFrame() LOCATIONIQ_TOKEN = "pk.e1561a89e1ed2bc2ddeb3ee53fd88fb8" geocoded_data = [] for index, row in df_clients.iterrows(): address = str(row['Adresse']).strip() client_id = row['ID_Client'] try: url = "https://us1.locationiq.com/v1/search.php" params = { 'key': LOCATIONIQ_TOKEN, 'q': address, 'format': 'json', 'countrycodes': 'sn', 'limit': 1, 'accept-language': 'fr' } response = requests.get(url, params=params, timeout=10) if response.status_code == 200: data = response.json() if data and len(data) > 0: result = data[0] lat = float(result['lat']) lon = float(result['lon']) geocoded_data.append({ "ID": client_id, "Nom": row.get('Nom_Complet', 'Client Inconnu'), "Adresse": address, "lat": lat, "lon": lon, "Revenus": row.get('Revenus_Mensuels', 0) }) time.sleep(1.2) except Exception as e: pass return pd.DataFrame(geocoded_data) # --- FONCTION PRINCIPALE --- def show_map_dashboard(client, sheet_name): # Appliquer les styles spécifiques apply_map_dashboard_styles() # Wrapper pour isolation des styles st.markdown('
', unsafe_allow_html=True) st.markdown("

GLOBAL CONTROL TOWER

", unsafe_allow_html=True) # === CHARGEMENT DES DONNÉES === try: sh = client.open(sheet_name) ws_clients = sh.worksheet("Clients_KYC") ws_prets = sh.worksheet("Prets_Master") df_raw = pd.DataFrame(ws_clients.get_all_records()) df_prets = pd.DataFrame(ws_prets.get_all_records()) except Exception as e: st.error(f"Erreur de lecture : {e}") st.markdown('
', unsafe_allow_html=True) return if df_raw.empty: st.info("Aucune donnée client.") st.markdown('', unsafe_allow_html=True) return # === 5 METRICS TEMPS RÉEL === col_m1, col_m2, col_m3, col_m4, col_m5 = st.columns(5) # Calculs depuis Prets_Master if not df_prets.empty: total_prets = len(df_prets) # Conversion sécurisée du montant en numérique if 'Montant_Capital' in df_prets.columns: montant_total = pd.to_numeric(df_prets['Montant_Capital'], errors='coerce').fillna(0).sum() else: montant_total = 0 prets_actifs = len(df_prets[df_prets['Statut'] == 'Actif']) if 'Statut' in df_prets.columns else 0 # Conversion sécurisée du taux en numérique if 'Taux_Hebdo' in df_prets.columns: taux_moyen = pd.to_numeric(df_prets['Taux_Hebdo'], errors='coerce').fillna(0).mean() else: taux_moyen = 0 prets_semaine = len(df_prets.tail(5)) if len(df_prets) >= 5 else len(df_prets) else: total_prets = montant_total = prets_actifs = taux_moyen = prets_semaine = 0 with col_m1: st.metric("PRÊTS ACTIFS", f"{prets_actifs}", f"+{prets_semaine} (7j)") with col_m2: st.metric("CAPITAL DÉPLOYÉ", f"{montant_total/1e6:.1f}M XOF", "+12%") with col_m3: st.metric("TAUX MOYEN", f"{taux_moyen:.1f}%", "-0.2%") with col_m4: st.metric("CLIENTS ACTIFS", f"{len(df_raw)}", "+3") with col_m5: st.metric("RECOUVREMENT", "94.2%", "+1.8%") st.markdown("---") # === GÉOCODAGE === with st.spinner(" Triangulation satellites..."): df_map = geocode_addresses(df_raw) if df_map.empty: st.warning("⚠️ Impossible de géolocaliser les adresses.") st.markdown('', unsafe_allow_html=True) return # === LAYOUT PRINCIPAL : 3 COLONNES === col_left, col_center, col_right = st.columns([1, 3, 1]) # === COLONNE GAUCHE : CLIENTS === with col_left: st.markdown("### TARGETS") st.caption(f"{len(df_map)} clients géolocalisés") # Session state pour focus if 'focus_lat' not in st.session_state: st.session_state['focus_lat'] = df_map['lat'].mean() st.session_state['focus_lon'] = df_map['lon'].mean() st.session_state['focus_id'] = None for idx, row in df_map.iterrows(): client_key = f"client_{row['ID']}" with st.expander(f"🔹 {row['ID']}", expanded=False): st.caption(f"{row['Nom']}") st.caption(f" {row['Adresse'][:35]}...") st.caption(f" {row['Revenus']:,.0f} XOF") if st.button("LOCALISER", key=f"loc_{client_key}", use_container_width=True): st.session_state['focus_lat'] = row['lat'] st.session_state['focus_lon'] = row['lon'] st.session_state['focus_id'] = row['ID'] st.rerun() # === COLONNE CENTRALE : CARTE === with col_center: st.markdown("### TACTICAL MAP") # Préparer données clients clients_js = [] for _, row in df_map.iterrows(): clients_js.append({ 'lat': row['lat'], 'lng': row['lon'], 'id': row['ID'], 'nom': row['Nom'], 'adresse': row['Adresse'] }) # Focus focus_lat = st.session_state.get('focus_lat', df_map['lat'].mean()) focus_lng = st.session_state.get('focus_lon', df_map['lon'].mean()) # HTML Mapbox mapbox_html = f"""
""" st.components.v1.html(mapbox_html, height=500) # === COLONNE DROITE : ALERTES === with col_right: st.markdown("### LIVE FEED") now = datetime.datetime.now() alerts = [ { 'type': 'critical', 'title': 'SEUIL ATTEINT', 'message': 'NPL > 15% détecté', 'time': (now - datetime.timedelta(minutes=2)).strftime('%H:%M') }, { 'type': 'warning', 'title': 'ANOMALIE DÉTECTÉE', 'message': 'CA mensuel -12%', 'time': (now - datetime.timedelta(minutes=15)).strftime('%H:%M') }, { 'type': 'success', 'title': 'VALIDATION', 'message': f'{df_map.iloc[0]["ID"]} approuvé', 'time': (now - datetime.timedelta(minutes=23)).strftime('%H:%M') }, { 'type': 'info', 'title': 'RECOMMANDATION', 'message': 'Augmenter taux +0.5%', 'time': (now - datetime.timedelta(hours=1)).strftime('%H:%M') }, { 'type': 'warning', 'title': 'RETARD PAIEMENT', 'message': '3 clients en J+7', 'time': (now - datetime.timedelta(hours=2)).strftime('%H:%M') }, ] for alert in alerts: st.markdown(f"""
{alert['title']} {alert['time']}
{alert['message']}
""", unsafe_allow_html=True) # === STATS ANSD === st.markdown("---") st.markdown("### INDICATEURS MACROÉCONOMIQUES ANSD") col1, col2, col3 = st.columns(3) with col1: st.metric("Croissance PIB", "5.2%", "+0.3%") st.metric("Taux BCEAO", "3.0%", "-0.5%") st.metric("NPL Bancaires", "14.8%", "+1.2%") with col2: st.metric("Emploi Informel", "68%", "-2%") st.metric("Dette Publique", "72% PIB", "+3%") st.metric("Transferts Diaspora", "2.1B USD", "+5%") with col3: st.metric("Inflation Alimentaire", "8.3%", "+1.5%") st.metric("Taux EUR/USD", "1.08", "-0.02") st.metric("Production Agricole", "92 idx", "-8 idx") # Fermeture du wrapper st.markdown('', unsafe_allow_html=True)