Vortex-Flux / src /modules /kyc_form.py
klydekushy's picture
Update src/modules/kyc_form.py
c741327 verified
raw
history blame
14.9 kB
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}")