Spaces:
Running
Running
| import streamlit as st | |
| import re | |
| from datetime import date, timedelta | |
| # --- FONCTIONS UTILITAIRES INTERNES --- | |
| def inject_pulsing_css(): | |
| st.markdown(""" | |
| <style> | |
| .field-label { | |
| font-weight: 600; | |
| color: #F5F8FA; | |
| margin-bottom: 5px; | |
| display: block; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| def lbl(text, mandatory=False): | |
| st.markdown(f'<span class="field-label">{text}</span>', unsafe_allow_html=True) | |
| # Listes de référence | |
| LISTE_PROFESSIONS = ["Commerçant", "Fonctionnaire", "Agriculteur", "Etudiant", "Ouvrier", "Cadre Supérieur", "Ingénieur", "Médecin", "Autre"] | |
| LISTE_QUARTIERS = [ | |
| "Plateau", "Médina", "Fann", "Point E", "Gueule Tapée", "Colobane", | |
| "Hann Maristes 1", "Hann Maristes 2", "Hann Bel-Air", | |
| "Yoff", "Ouakam", "Ngor", "Almadies", "Mamelles", | |
| "Mermoz", "Sacré-Cœur", "Liberté 1", "Liberté 2", "Liberté 3", | |
| "Liberté 4", "Liberté 5", "Liberté 6", | |
| "HLM", "Grand Dakar", "Dieuppeul", "Derklé", | |
| "Pikine Est", "Pikine Ouest", "Pikine Nord", "Pikine Sud", | |
| "Guinaw Rails", "Thiaroye", "Dalifort", | |
| "Guédiawaye", "Golf Sud", "Medina Gounass", "Wakhinane Nimzatt", | |
| "Rufisque", "Bargny", "Diamniadio", "Sébikotane", "Zac Mbao" | |
| "Autre" | |
| ] | |
| LISTE_VILLES = [ | |
| "Dakar", "Pikine", "Guédiawaye", "Rufisque", | |
| "Thiès", "Mbour", "Tivaouane", "Joal-Fadiouth", "Popenguine", | |
| "Touba", "Diourbel", "Kaolack", "Fatick", "Foundiougne", | |
| "Saint-Louis", "Dagana", "Podor", "Richard-Toll", | |
| "Ziguinchor", "Oussouye", "Bignona", "Kolda", "Sédhiou", | |
| "Tambacounda", "Kédougou", "Bakel", "Koumpentoum", | |
| "Autre" | |
| ] | |
| LISTE_PAYS = ["Sénégal", "Gabon", "Congo", "Guinée", "Côte d'Ivoire", "Mali", "Tchad", "France", "Autre"] | |
| # --- FONCTION PRINCIPALE APPELÉE PAR L'APP --- | |
| def show_kyc_form(client, sheet_name, generate_id_func): | |
| """ | |
| Affiche le formulaire KYC (Client ou Garant). | |
| """ | |
| # inject_pulsing_css() # Décommenter si le CSS n'est pas chargé ailleurs | |
| # Sélecteur de Type (Client ou Garant) | |
| type_entite = st.radio("Type d'enregistrement", ["Client Physique", "Garant Physique"], horizontal=True) | |
| # Configuration dynamique selon le choix | |
| if type_entite == "Client Physique": | |
| prefix_id = "CLI" | |
| target_sheet = "Clients_KYC" | |
| header_title = "ENTITÉ : NOUVEAU CLIENT" | |
| else: | |
| prefix_id = "GAR" | |
| target_sheet = "Garants_KYC" | |
| header_title = "ENTITÉ : NOUVEAU GARANT" | |
| st.header(header_title) | |
| # Appel de la fonction parente pour l'ID avec les bons paramètres | |
| new_id = generate_id_func(prefix_id, target_sheet) | |
| st.caption(f"Système ID : {new_id}") | |
| with st.form("kyc_form_module", clear_on_submit=False): | |
| # --- BLOC 1 : IDENTITÉ --- | |
| st.markdown("### IDENTITÉ & CONTACT") | |
| c1, c2, c3 = st.columns(3) | |
| with c1: | |
| lbl("Nom Complet", True) | |
| nom = st.text_input("Nom", label_visibility="collapsed") | |
| lbl("Genre", True) # ← NOUVEAU | |
| genre = st.selectbox("Genre", ["Homme", "Femme"], label_visibility="collapsed") # ← NOUVEAU | |
| lbl("Date de Naissance", True) | |
| date_naiss = st.date_input("Date Naiss", value=date.today()-timedelta(days=365*18), min_value=date.today()-timedelta(days=365*100), max_value=date.today()-timedelta(days=365*18), label_visibility="collapsed") | |
| with c2: | |
| lbl("Adresse", True) | |
| adresse = st.text_input("Adresse", label_visibility="collapsed") | |
| lbl("Téléphone", True) | |
| telephone = st.text_input("Tel", label_visibility="collapsed") | |
| with c3: | |
| lbl("Email", True) | |
| email = st.text_input("Email", label_visibility="collapsed").lower() | |
| lbl("État Civil", True) | |
| etat_civil = st.selectbox("Etat", ["Célibataire", "Marié(e)", "Divorcé(e)", "Veuf(ve)"], label_visibility="collapsed") | |
| # --- BLOC 2 : DOCUMENTS --- | |
| st.markdown("### DOCUMENTS OFFICIELS") | |
| c4, c5, c6 = st.columns(3) | |
| with c4: | |
| lbl("Type Pièce", True) | |
| type_id = st.selectbox("Type ID", ["CNI", "Passeport", "Carte d'étranger", "Carte Consulaire", "Permis de conduire", "Carte Electeur"], label_visibility="collapsed") | |
| with c5: | |
| lbl("Numéro ID", True) | |
| id_officiel = st.text_input("Num ID", label_visibility="collapsed") | |
| with c6: | |
| lbl("Expiration", True) | |
| date_exp_id = st.date_input("Exp ID", value=date.today()+timedelta(days=90), min_value=date.today(), label_visibility="collapsed") | |
| # --- BLOC 3 : PRO --- | |
| st.markdown("### PROFESSION") | |
| c7, c8, c9 = st.columns(3) | |
| with c7: | |
| lbl("Statut Pro", True) | |
| statut_pro_select = st.selectbox("Statut", ["Salarié", "Indépendant", "Entrepreneur", "Fonctionnaire", "Etudiant", "Sans_Emploi", "Retraité", "Autre"], label_visibility="collapsed") # ← MODIFIÉ | |
| statut_pro_autre = st.text_input("Si Autre, préciser", key="statut_pro_autre", placeholder="Statut personnalisé...") # ← NOUVEAU | |
| lbl("Profession", True) | |
| prof_select = st.selectbox("Choix Profession", LISTE_PROFESSIONS, label_visibility="collapsed") | |
| prof_autre = st.text_input("Si Autre, préciser", key="prof_autre", placeholder="Profession personnalisée...") | |
| with c8: | |
| lbl("Employeur", False) | |
| employeur = st.text_input("Employeur", label_visibility="collapsed") | |
| lbl("Secteur d'Activité", True) | |
| secteur_select = st.selectbox("Secteur", ["Commerce", "Reseaux et Telecommunication", "Services", "Agriculture", "Etudiant", "Industrie", "Sante", "Education", "Transport", "Autre"], label_visibility="collapsed") | |
| secteur_autre = st.text_input("Si Autre, préciser", key="secteur_autre", placeholder="Secteur personnalisé...") | |
| with c9: | |
| lbl("Pers. à Charge", True) | |
| pers_charge = st.number_input("Charge", min_value=0, step=1, max_value=3, label_visibility="collapsed") | |
| lbl("Ancienneté Statut pro (mois)", True) | |
| anciennete_emploi = st.number_input("Ancienneté Emploi", min_value=3, step=1, label_visibility="collapsed") | |
| # --- BLOC 4 : FINANCES --- | |
| st.markdown("### FINANCES (XOF)") | |
| c10, c11, c12 = st.columns(3) | |
| with c10: | |
| lbl("Revenus Mensuels", True) | |
| revenus = st.number_input("Rev. Mensuel", min_value=0.0, step=10000.0, format="%.2f", value=0.0, label_visibility="collapsed") | |
| lbl("Autres Revenus", False) | |
| autres_rev = st.number_input("Autre Rev.", min_value=0.0, step=5000.0, format="%.2f", value=0.0, label_visibility="collapsed") | |
| with c11: | |
| lbl("Ancienneté Revenus (mois)", False) | |
| anciennete_revenu = st.number_input("Ancienneté Rev.", min_value=0, step=1, value=0, label_visibility="collapsed") | |
| lbl("Autres Sources Revenus", False) | |
| autres_sources = st.text_input("Préciser Sources", label_visibility="collapsed") | |
| with c12: | |
| lbl("Charges Estimées", False) | |
| charges = st.number_input("Charges", min_value=0.0, step=5000.0, format="%.2f", value=0.0, label_visibility="collapsed") | |
| lbl("Patrimoine Déclaré", False) | |
| patrimoine = st.number_input("Patrimoine", min_value=0.0, step=100000.0, format="%.2f", value=0.0, label_visibility="collapsed") | |
| # --- BLOC 5 : LOCALISATION --- | |
| st.markdown("### LOCALISATION") | |
| c13, c14, c15 = st.columns(3) | |
| with c13: | |
| lbl("Statut Logement", True) | |
| statut_log = st.selectbox("Logement", ["Propriétaire", "Locataire", "Hébergé"], label_visibility="collapsed") | |
| lbl("Quartier", True) | |
| quartier_select = st.selectbox("Quartier", LISTE_QUARTIERS, label_visibility="collapsed") | |
| quartier_autre = st.text_input("Si Autre, préciser", key="quartier_autre", placeholder="Quartier personnalisé...") | |
| with c14: | |
| lbl("Ville", True) | |
| ville_select = st.selectbox("Ville", LISTE_VILLES, label_visibility="collapsed") | |
| ville_autre = st.text_input("Si Autre, préciser", key="ville_autre", placeholder="Ville personnalisée...") | |
| lbl("Pays de Résidence", True) | |
| pays_residence_select = st.selectbox("Pays Résidence", LISTE_PAYS, label_visibility="collapsed") | |
| pays_residence_autre = st.text_input("Si Autre, préciser", key="pays_res_autre", placeholder="Pays personnalisé...") | |
| with c15: | |
| lbl("Pays de Naissance", True) | |
| pays_naissance_select = st.selectbox("Pays Naissance", LISTE_PAYS, key="pays_naiss", label_visibility="collapsed") | |
| pays_naissance_autre = st.text_input("Si Autre, préciser", key="pays_naiss_autre", placeholder="Pays personnalisé...") | |
| lbl("N° Fiscal", False) | |
| n_fiscal = st.text_input("Fiscal", label_visibility="collapsed") | |
| # --- BLOC 6 : CONFORMITÉ --- | |
| st.markdown("### CONFORMITÉ & BANQUE") | |
| c16, c17 = st.columns(2) | |
| with c16: | |
| lbl("Moyen Transfert", True) | |
| transfert = st.selectbox("Transfert", ["Virement", "Mobile_Money", "Cash", "Chèque"], label_visibility="collapsed") | |
| lbl("Entité Financière (Banque)", True) | |
| entite_financiere = st.text_input("Banque", label_visibility="collapsed") | |
| with c17: | |
| lbl("Origine Fonds", True) | |
| origine = st.selectbox("Origine", ["Salaire", "Entreprise", "Contribution Familiale", "Epargne", "Héritage", "Autre"], label_visibility="collapsed") | |
| lbl("Vérification AML", True) | |
| aml = st.selectbox("AML", ["Non_Fait", "OK", "Match", "Review"], label_visibility="collapsed") | |
| # --- BLOC 7 : RÉSEAUX SOCIAUX --- | |
| st.markdown("### RÉSEAUX SOCIAUX") | |
| c18, c19, c20 = st.columns(3) | |
| with c18: | |
| lbl("Facebook", False) | |
| facebook = st.text_input("Lien Facebook", placeholder="https://facebook.com/...", label_visibility="collapsed") | |
| lbl("Instagram", False) | |
| instagram = st.text_input("Lien Instagram", placeholder="https://instagram.com/...", label_visibility="collapsed") | |
| with c19: | |
| lbl("LinkedIn", False) | |
| linkedin = st.text_input("Lien LinkedIn", placeholder="https://linkedin.com/in/...", label_visibility="collapsed") | |
| lbl("Twitter / X", False) | |
| twitter = st.text_input("Lien Twitter/X", placeholder="https://twitter.com/...", label_visibility="collapsed") | |
| with c20: | |
| lbl("TikTok", False) | |
| tiktok = st.text_input("Lien TikTok", placeholder="https://tiktok.com/@...", label_visibility="collapsed") | |
| lbl("WhatsApp", False) | |
| whatsapp = st.text_input("WhatsApp", placeholder="wa.me/+221XXXXXXXXX", label_visibility="collapsed") | |
| st.divider() | |
| lbl("Commentaires / Notes", False) | |
| notes = st.text_area("Notes", label_visibility="collapsed") | |
| # Bouton dynamique selon le type | |
| label_btn = f"VÉRIFIER ET ENREGISTRER LE {prefix_id}" | |
| submit_btn = st.form_submit_button(label_btn) | |
| # --- LOGIQUE DE VALIDATION ET ENVOI --- | |
| if submit_btn: | |
| errors = [] | |
| if not re.match(r"^[a-zA-Z\s\-\']{2,100}$", nom): errors.append("❌ Nom invalide.") | |
| clean_phone = re.sub(r"[\s\-\.]", "", telephone) | |
| if not re.match(r"^(\+|00)?221(7[0678]|33)[0-9]{7}$", clean_phone) and not re.match(r"^\+?[0-9]{9,15}$", clean_phone): errors.append("❌ Téléphone invalide.") | |
| if not re.match(r"[^@]+@[^@]+\.[^@]+", email): errors.append("❌ Email invalide.") | |
| if " " in id_officiel or len(id_officiel) < 5: errors.append("❌ ID Officiel invalide.") | |
| if statut_pro_select in ["Salarié", "Fonctionnaire"] and len(employeur) < 2: errors.append("❌ Employeur obligatoire.") | |
| # --- LOGIQUE DE RANGEMENT --- | |
| if errors: | |
| for e in errors: st.error(e) | |
| else: | |
| try: | |
| # Calcul statut_pro avec gestion "Autre" | |
| statut_pro_val = statut_pro_autre.strip().upper() if statut_pro_select == "Autre" and statut_pro_autre.strip() else statut_pro_select.upper() | |
| # Calcul des valeurs "Autre" avant préparation de la ligne | |
| prof_val = prof_autre.strip().upper() if prof_select == "Autre" and prof_autre.strip() else prof_select.upper() | |
| quartier_val = quartier_autre.strip().upper() if quartier_select == "Autre" and quartier_autre.strip() else quartier_select.upper() | |
| ville_val = ville_autre.strip().upper() if ville_select == "Autre" and ville_autre.strip() else ville_select.upper() | |
| pays_residence_val = pays_residence_autre.strip().upper() if pays_residence_select == "Autre" and pays_residence_autre.strip() else pays_residence_select.upper() | |
| pays_naissance_val = pays_naissance_autre.strip().upper() if pays_naissance_select == "Autre" and pays_naissance_autre.strip() else pays_naissance_select.upper() | |
| # Calcul secteur avec gestion "Autre" | |
| secteur = secteur_autre.strip().upper() if secteur_select == "Autre" and secteur_autre.strip() else secteur_select.upper() | |
| # Concaténation des réseaux sociaux | |
| reseaux_sociaux = f"FB: {facebook} | IG: {instagram} | LI: {linkedin} | TW: {twitter} | TT: {tiktok} | WA: {whatsapp}".strip() | |
| # Préparation de la ligne DANS L'ORDRE EXACT DES COLONNES GOOGLE SHEET | |
| row_to_add = [ | |
| new_id, # ID_Client ou ID_Garant | |
| nom.upper(), # Nom_Complet | |
| genre, # Genre ← NOUVEAU | |
| date_naiss.strftime("%d-%m-%Y"), # Date_Naissance | |
| adresse, # Adresse | |
| telephone, # Telephone | |
| email, # Email | |
| type_id, # Type_Piece_Identite | |
| id_officiel.upper(), # ID_Officiel | |
| date_exp_id.strftime("%d-%m-%Y"),# Date_Expiration_ID | |
| etat_civil, # Etat_Civil | |
| pers_charge, # Pers_Charge | |
| prof_val, # Profession | |
| statut_pro_val, # Statut_Pro ← MODIFIÉ | |
| employeur.upper(), # Employeur | |
| secteur, # Secteur_Activite | |
| anciennete_emploi, # Anciennete_Emploi | |
| revenus, # Revenus_Mensuels | |
| autres_rev, # Autres_Revenus | |
| anciennete_revenu, # Anciennete_Revenu | |
| autres_sources, # Autres_Sources_Revenu | |
| charges, # Charges_Estimees | |
| patrimoine, # Patrimoine_Declare | |
| statut_log, # Statut_Logement | |
| quartier_val, # Quartier | |
| ville_val, # Ville | |
| pays_residence_val, # Pays_Residence | |
| transfert, # Moyen_Transfert | |
| entite_financiere.upper(), # Entite_Financiere | |
| n_fiscal, # Numero_Fiscal | |
| pays_naissance_val, # Pays_Naissance | |
| aml, # Verification_AML | |
| origine, # Origine_Fonds | |
| reseaux_sociaux, # Reseau_sociaux | |
| notes, # Commentaires_Notes | |
| date.today().strftime("%d-%m-%Y") # Date_Creation ← NOUVEAU | |
| ] | |
| # ÉCRITURE VIA LA CONNEXION PASSÉE EN ARGUMENT | |
| sh = client.open(sheet_name) | |
| # On cible la feuille dynamiquement selon le type | |
| worksheet = sh.worksheet(target_sheet) | |
| worksheet.append_row(row_to_add) | |
| st.success(f"♦️ {new_id} VALIDÉ ET ENREGISTRÉ DANS {target_sheet} !") | |
| except Exception as e: | |
| st.error(f"🔻 Erreur technique module KYC : {e}") |