Spaces:
Running
Running
| import streamlit as st | |
| import re | |
| from datetime import date, timedelta | |
| # --- FONCTIONS UTILITAIRES INTERNES --- | |
| def inject_pulsing_css(): | |
| st.markdown(""" | |
| <style> | |
| @keyframes pulse-red { | |
| 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 82, 82, 0.7); } | |
| 70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(255, 82, 82, 0); } | |
| 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 82, 82, 0); } | |
| } | |
| .mandatory-dot { | |
| display: inline-block; | |
| width: 8px; | |
| height: 8px; | |
| background-color: #ff5252; | |
| border-radius: 50%; | |
| margin-left: 8px; | |
| animation: pulse-red 2s infinite; | |
| vertical-align: middle; | |
| } | |
| .field-label { font-weight: 600; color: #F5F8FA; margin-bottom: 5px; display: block; } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| def lbl(text, mandatory=False): | |
| if mandatory: | |
| st.markdown(f'<span class="field-label">{text} <span class="mandatory-dot" title="Obligatoire"></span></span>', unsafe_allow_html=True) | |
| else: | |
| 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 = [ | |
| # 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}") | |