import streamlit as st import re from datetime import date, timedelta # --- FONCTIONS UTILITAIRES INTERNES --- def inject_pulsing_css(): st.markdown(""" """, unsafe_allow_html=True) def lbl(text, mandatory=False): if mandatory: st.markdown(f'{text} ', unsafe_allow_html=True) else: st.markdown(f'{text}', 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 = [ # Dakar Plateau "Plateau", "Médina", "Fann", "Point E", "Gueule Tapée", "Colobane", # Dakar Est "Hann Maristes 1", "Hann Maristes 2", "Hann Bel-Air", # Dakar Ouest "Yoff", "Ouakam", "Ngor", "Almadies", "Mamelles", # Dakar Centre "Mermoz", "Sacré-Cœur", "Liberté 1", "Liberté 2", "Liberté 3", "Liberté 4", "Liberté 5", "Liberté 6", "HLM", "Grand Dakar", "Dieuppeul", "Derklé", # Pikine "Pikine Est", "Pikine Ouest", "Pikine Nord", "Pikine Sud", "Guinaw Rails", "Thiaroye", "Dalifort", # Guédiawaye "Guédiawaye", "Golf Sud", "Medina Gounass", "Wakhinane Nimzatt", # Rufisque "Rufisque", "Bargny", "Diamniadio", "Sébikotane", # Autres "Autre" ] LISTE_VILLES = [ # Dakar & Région "Dakar", "Pikine", "Guédiawaye", "Rufisque", # Ouest "Thiès", "Mbour", "Tivaouane", "Joal-Fadiouth", "Popenguine", # Centre "Touba", "Diourbel", "Kaolack", "Fatick", "Foundiougne", # Nord "Saint-Louis", "Dagana", "Podor", "Richard-Toll", # Sud "Ziguinchor", "Oussouye", "Bignona", "Kolda", "Sédhiou", # Est "Tambacounda", "Kédougou", "Bakel", "Koumpentoum", # Autres "Autre" ] LISTE_PAYS = ["Sénégal", "France", "Côte d'Ivoire", "Mali", "États-Unis", "Autre"] # --- FONCTION PRINCIPALE APPELÉE PAR L'APP --- def show_kyc_form(client, sheet_name, generate_id_func): """ Affiche le formulaire KYC. Args: client: La connexion gspread active (passée depuis main). sheet_name: Le nom du fichier Google Sheet. generate_id_func: La fonction pour générer l'ID (passée depuis main). """ inject_pulsing_css() st.header("ENTITÉ : NOUVEL OBJET CLIENT") # Appel de la fonction parente pour l'ID new_id = generate_id_func("CLI", "Clients_KYC") st.caption(f"Système ID : {new_id}") type_personne = st.radio("Nature", ["Personne Physique", "Personne Morale"], horizontal=True) if type_personne == "Personne Morale": st.warning("⚠️ Module Personne Morale en construction.") 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("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 = st.selectbox("Statut", ["Salarié", "Indépendant", "Fonctionnaire", "Etudiant", "Sans_Emploi", "Retraité"], label_visibility="collapsed") 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 = st.selectbox("Secteur", ["Commerce", "Services", "Agriculture", "Etudiant", "Autre"], label_visibility="collapsed") 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é Emploi (mois)", True) anciennete_emploi = st.number_input("Ancienneté Emploi", min_value=3, step=1, label_visibility="collapsed") # --- BLOC 4 : FINANCES --- # --- 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", "Commerce", "Epargne", "Héritage", "Autre"], label_visibility="collapsed") lbl("Vérification AML", True) aml = st.selectbox("AML", ["Non_Fait", "OK", "Match", "Review"], label_visibility="collapsed") st.divider() lbl("Commentaires / Notes", False) notes = st.text_area("Notes", label_visibility="collapsed") submit_btn = st.form_submit_button("VÉRIFIER ET ENREGISTRER") # --- 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 in ["Salarié", "Fonctionnaire"] and len(employeur) < 2: errors.append("❌ Employeur obligatoire.") if errors: for e in errors: st.error(e) else: try: # 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() # Préparation de la ligne DANS L'ORDRE EXACT DES COLONNES GOOGLE SHEET row_to_add = [ new_id, # ID_Client nom.upper(), # Nom_Complet 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 (calculé ci-dessus) statut_pro, # Statut_Pro 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 (calculé ci-dessus) ville_val, # Ville (calculé ci-dessus) pays_residence_val, # Pays_Residence (calculé ci-dessus) transfert, # Moyen_Transfert entite_financiere.upper(), # Entite_Financiere n_fiscal, # Numero_Fiscal pays_naissance_val, # Pays_Naissance (calculé ci-dessus) aml, # Verification_AML origine, # Origine_Fonds notes # Commentaires_Notes ] # ÉCRITURE VIA LA CONNEXION PASSÉE EN ARGUMENT sh = client.open(sheet_name) worksheet = sh.worksheet("Clients_KYC") worksheet.append_row(row_to_add) st.success(f"🟢 CLIENT {new_id} VALIDÉ ET ENREGISTRÉ !") st.balloons() except Exception as e: st.error(f"🔴 Erreur technique module KYC : {e}")