diff --git a/commandes/panel.py b/commandes/panel.py index c3f0ef5..a1b524e 100644 --- a/commandes/panel.py +++ b/commandes/panel.py @@ -1,6 +1,7 @@ import os import json import random +import hashlib import discord from discord import app_commands @@ -17,7 +18,6 @@ from utils.guild_config import ( CATEGORY_LABELS, ) from utils.bank_manager import BankManager -from utils.rib import decoder_rib bank_manager = BankManager() @@ -33,12 +33,8 @@ C_CONFIG = 0x95A5A6 C_FUN = 0xE67E22 -def _load_json(name): - path = os.path.join(base_dir, "data", name) - if os.path.exists(path): - with open(path, "r", encoding="utf-8") as f: - return json.load(f) - return {} +def _uid(seed: str) -> str: + return hashlib.md5(seed.encode()).hexdigest()[:12] def _btn(label, style, cid, cb): @@ -51,31 +47,22 @@ def _back_btn(cid, cb): return _btn("◀ Retour", discord.ButtonStyle.secondary, cid, cb) -# ═══════════════════════════════════════════════════════════════════ -# USER SELECT (pour banquiers) -# ═══════════════════════════════════════════════════════════════════ - -class TargetUserSelect(discord.ui.UserSelect): - def __init__(self, custom_id="bp_usel"): - super().__init__(placeholder="Sélectionner un membre...", custom_id=custom_id) - self.selected_user = None - - async def callback(self, interaction: discord.Interaction): - self.selected_user = self.values[0] - await interaction.response.defer() +async def _get_user_bank(guild, user) -> dict: + banques = await bank_manager.lister_banques(guild.id) + for b in banques: + role = guild.get_role(b["role_id"]) + if role and role in user.roles: + return b + return None # ═══════════════════════════════════════════════════════════════════ -# MODALS — COMPTES (Mes Comptes) +# MODALS # ═══════════════════════════════════════════════════════════════════ class VirementModal(discord.ui.Modal, title="💸 Virement"): - rib_dest = discord.ui.TextInput( - label="RIB du destinataire", required=True, max_length=30 - ) - montant = discord.ui.TextInput( - label="Montant (€)", required=True, max_length=15 - ) + rib_dest = discord.ui.TextInput(label="RIB destinataire", required=True, max_length=30) + montant = discord.ui.TextInput(label="Montant (EUR)", required=True, max_length=15) def __init__(self, source_rib=None): super().__init__() @@ -85,7 +72,7 @@ class VirementModal(discord.ui.Modal, title="💸 Virement"): try: montant = float(self.montant.value.replace(",", ".").strip()) except ValueError: - await interaction.response.send_message("❌ Montant invalide.", ephemeral=True) + await interaction.response.send_message("Montant invalide.", ephemeral=True) return rib_dest = self.rib_dest.value.strip().replace(" ", "") try: @@ -96,21 +83,19 @@ class VirementModal(discord.ui.Modal, title="💸 Virement"): montant=montant, emetteur_id=interaction.user.id, ) - msg = f"✅ Virement de **{montant:.2f}€** envoyé." + msg = f"Virement de **{montant:.2f}EUR** envoye." if result["tva_prelevee"] > 0: - msg += f"\n💡 TVA prélevée : **{result['tva_prelevee']:.2f}€**" + msg += f"\nTVA prelevee : **{result['tva_prelevee']:.2f}EUR**" await interaction.response.send_message(msg, ephemeral=True) except ValueError as e: - await interaction.response.send_message(f"❌ {e}", ephemeral=True) + await interaction.response.send_message(str(e), ephemeral=True) class DepotRetraitModal(discord.ui.Modal): - montant = discord.ui.TextInput( - label="Montant (€)", required=True, max_length=15 - ) + montant = discord.ui.TextInput(label="Montant (EUR)", required=True, max_length=15) def __init__(self, rib: str, operation: str): - super().__init__(title=f"{'➕ Dépôt' if operation == 'depot' else '➖ Retrait'}") + super().__init__(title=f"{'Depot' if operation == 'depot' else 'Retrait'}") self.rib = rib self.operation = operation @@ -118,53 +103,43 @@ class DepotRetraitModal(discord.ui.Modal): try: montant = float(self.montant.value.replace(",", ".").strip()) except ValueError: - await interaction.response.send_message("❌ Montant invalide.", ephemeral=True) + await interaction.response.send_message("Montant invalide.", ephemeral=True) return try: if self.operation == "depot": - result = await bank_manager.depot_banquier( - interaction.guild.id, self.rib, montant, interaction.user.id - ) + result = await bank_manager.depot_banquier(interaction.guild.id, self.rib, montant, interaction.user.id) else: - result = await bank_manager.retrait_banquier( - interaction.guild.id, self.rib, montant, interaction.user.id - ) + result = await bank_manager.retrait_banquier(interaction.guild.id, self.rib, montant, interaction.user.id) await interaction.response.send_message( - f"✅ {self.operation.title()} de **{montant:.2f}€**. Nouveau solde : **{result['nouveau_solde']:.2f}€**", + f"{self.operation.title()} de **{montant:.2f}EUR**. Nouveau solde : **{result['nouveau_solde']:.2f}EUR**", ephemeral=True, ) except ValueError as e: - await interaction.response.send_message(f"❌ {e}", ephemeral=True) + await interaction.response.send_message(str(e), ephemeral=True) -# ═══════════════════════════════════════════════════════════════════ -# MODALS — BANQUIER -# ═══════════════════════════════════════════════════════════════════ +class OuvrirCompteModal(discord.ui.Modal, title="Ouvrir un Compte"): + membre_id = discord.ui.TextInput(label="ID du membre", required=True, max_length=20) + type_compte = discord.ui.TextInput(label="Type (particulier / entreprise)", required=True, max_length=15) + taux_tva = discord.ui.TextInput(label="TVA % (entreprise, 0 sinon)", required=False, max_length=6) -class OuvrirCompteModal(discord.ui.Modal, title="🏦 Ouvrir un Compte"): - membre_id = discord.ui.TextInput( - label="ID du membre", required=True, max_length=20 - ) - type_compte = discord.ui.TextInput( - label="Type (particulier / entreprise)", required=True, max_length=15 - ) - taux_tva = discord.ui.TextInput( - label="TVA % (entreprise, 0 sinon)", required=False, max_length=6 - ) + def __init__(self, bank_data=None): + super().__init__() + self.bank_data = bank_data async def on_submit(self, interaction: discord.Interaction): try: membre_id = int(self.membre_id.value.strip()) except ValueError: - await interaction.response.send_message("❌ ID invalide.", ephemeral=True) + await interaction.response.send_message("ID invalide.", ephemeral=True) return member = interaction.guild.get_member(membre_id) if not member: - await interaction.response.send_message("❌ Membre introuvable.", ephemeral=True) + await interaction.response.send_message("Membre introuvable.", ephemeral=True) return type_val = self.type_compte.value.strip().lower() if type_val not in ("particulier", "entreprise"): - await interaction.response.send_message("❌ Type invalide.", ephemeral=True) + await interaction.response.send_message("Type invalide.", ephemeral=True) return tva = 0.0 if type_val == "entreprise" and self.taux_tva.value: @@ -173,23 +148,17 @@ class OuvrirCompteModal(discord.ui.Modal, title="🏦 Ouvrir un Compte"): except ValueError: pass - banques = await bank_manager.lister_banques(interaction.guild.id) - bank = None - for b in banques: - role = interaction.guild.get_role(b["role_id"]) - if role and role in interaction.user.roles: - bank = b - break + bank = self.bank_data if not bank: - await interaction.response.send_message("❌ Aucune banque associée à vos rôles.", ephemeral=True) + bank = await _get_user_bank(interaction.guild, interaction.user) + if not bank: + await interaction.response.send_message("Aucune banque associee.", ephemeral=True) return try: - result = await bank_manager.ouvrir_compte( - interaction.guild.id, bank["id"], member.id, type_val, tva - ) + result = await bank_manager.ouvrir_compte(interaction.guild.id, bank["id"], member.id, type_val, tva) embed = discord.Embed( - title="💳 Compte ouvert", + title="Compte ouvert", description=( f"**Banque :** {bank['name']}\n" f"**Membre :** {member.mention}\n" @@ -201,157 +170,56 @@ class OuvrirCompteModal(discord.ui.Modal, title="🏦 Ouvrir un Compte"): await interaction.response.send_message(embed=embed, ephemeral=True) try: await member.send( - f"💳 Votre compte **{type_val}** chez **{bank['name']}** a été ouvert.\n" + f"Votre compte **{type_val}** chez **{bank['name']}** a ete ouvert.\n" f"**RIB :** `{result['rib']}`" ) except discord.Forbidden: pass except ValueError as e: - await interaction.response.send_message(f"❌ {e}", ephemeral=True) + await interaction.response.send_message(str(e), ephemeral=True) -class TransactionsModal(discord.ui.Modal, title="📜 Transactions"): - rib = discord.ui.TextInput( - label="RIB du compte", required=True, max_length=30 - ) - - async def on_submit(self, interaction: discord.Interaction): - rib = self.rib.value.strip().replace(" ", "") - txs = await bank_manager.get_transactions(interaction.guild.id, rib, limit=10) - if not txs: - await interaction.response.send_message("Aucune transaction.", ephemeral=True) - return - lines = [] - for tx in txs: - lines.append( - f"`{tx['date']}` — **{tx['type']}** — {tx['montant']:.2f}€" - ) - embed = discord.Embed( - title=f"📜 Transactions ({rib})", - description="\n".join(lines), - color=discord.Color.blue(), - ) - await interaction.response.send_message(embed=embed, ephemeral=True) - - -# ═══════════════════════════════════════════════════════════════════ -# MODALS — GOUVERNEMENT -# ═══════════════════════════════════════════════════════════════════ - -class GouvVirerModal(discord.ui.Modal, title="💸 Virement Gouvernement"): - rib_dest = discord.ui.TextInput( - label="RIB du destinataire", required=True, max_length=30 - ) - montant = discord.ui.TextInput( - label="Montant (€)", required=True, max_length=15 - ) +class CreerBanqueModal(discord.ui.Modal, title="Creer une Banque"): + nom = discord.ui.TextInput(label="Nom de la banque", required=True, max_length=50) + role_id = discord.ui.TextInput(label="ID du role banquier", required=True, max_length=20) async def on_submit(self, interaction: discord.Interaction): if not has_role(interaction.user, "gouvernement"): - await interaction.response.send_message("❌ Réservé au gouvernement.", ephemeral=True) - return - try: - montant = float(self.montant.value.replace(",", ".").strip()) - except ValueError: - await interaction.response.send_message("❌ Montant invalide.", ephemeral=True) - return - rib_dest = self.rib_dest.value.strip().replace(" ", "") - try: - fisc = await bank_manager.get_fisc_config(interaction.guild.id) - if not fisc: - await interaction.response.send_message("❌ Aucun fisc configuré.", ephemeral=True) - return - source_compte = await bank_manager.get_compte_by_rib(interaction.guild.id, fisc.get("rib", "")) - if not source_compte: - await interaction.response.send_message("❌ Compte fisc introuvable.", ephemeral=True) - return - result = await bank_manager.virement( - guild_id=interaction.guild.id, - rib_source=source_compte["rib"], - rib_dest=rib_dest, - montant=montant, - emetteur_id=interaction.user.id, - ) - await interaction.response.send_message( - f"✅ Virement de **{montant:.2f}€** effectué depuis le Fisc.", - ephemeral=True, - ) - except ValueError as e: - await interaction.response.send_message(f"❌ {e}", ephemeral=True) - - -class ConfigFiscModal(discord.ui.Modal, title="🏛️ Configurer le Fisc"): - institution_id = discord.ui.TextInput( - label="ID de l'institution fisc", required=True, max_length=10 - ) - - async def on_submit(self, interaction: discord.Interaction): - if not has_role(interaction.user, "gouvernement"): - await interaction.response.send_message("❌ Réservé au gouvernement.", ephemeral=True) - return - try: - iid = int(self.institution_id.value.strip()) - except ValueError: - await interaction.response.send_message("❌ ID invalide.", ephemeral=True) - return - ok = await bank_manager.configurer_fisc(interaction.guild.id, iid) - if ok: - await interaction.response.send_message("✅ Fisc configuré.", ephemeral=True) - else: - await interaction.response.send_message("❌ Institution introuvable.", ephemeral=True) - - -class CreerBanqueModal(discord.ui.Modal, title="🏦 Créer une Banque"): - nom = discord.ui.TextInput( - label="Nom de la banque", required=True, max_length=50 - ) - role_id = discord.ui.TextInput( - label="ID du rôle banquier", required=True, max_length=20 - ) - - async def on_submit(self, interaction: discord.Interaction): - if not has_role(interaction.user, "gouvernement"): - await interaction.response.send_message("❌ Réservé au gouvernement.", ephemeral=True) + await interaction.response.send_message("Reserve au gouvernement.", ephemeral=True) return try: rid = int(self.role_id.value.strip()) except ValueError: - await interaction.response.send_message("❌ ID invalide.", ephemeral=True) + await interaction.response.send_message("ID invalide.", ephemeral=True) return role = interaction.guild.get_role(rid) if not role: - await interaction.response.send_message("❌ Rôle introuvable.", ephemeral=True) + await interaction.response.send_message("Role introuvable.", ephemeral=True) return try: result = await bank_manager.creer_banque(interaction.guild.id, self.nom.value.strip(), rid) embed = discord.Embed( - title="🏦 Banque créée", + title="Banque creee", description=( f"**Nom :** {result['name']}\n" - f"**Rôle :** {role.mention}\n" - f"**Code établissement :** `{result['establishment_code']}`" + f"**Role :** {role.mention}\n" + f"**Code etablissement :** `{result['establishment_code']}`" ), color=discord.Color.green(), ) await interaction.response.send_message(embed=embed, ephemeral=True) except ValueError as e: - await interaction.response.send_message(f"❌ {e}", ephemeral=True) + await interaction.response.send_message(str(e), ephemeral=True) -class CreerInstitutionModal(discord.ui.Modal, title="🏛️ Créer une Institution"): - nom = discord.ui.TextInput( - label="Nom de l'institution", required=True, max_length=50 - ) - role_id = discord.ui.TextInput( - label="ID du rôle (optionnel)", required=False, max_length=20 - ) - audit_all = discord.ui.TextInput( - label="Audit global ? (oui/non)", required=False, max_length=3 - ) +class CreerInstitutionModal(discord.ui.Modal, title="Creer une Institution"): + nom = discord.ui.TextInput(label="Nom de l'institution", required=True, max_length=50) + role_id = discord.ui.TextInput(label="ID du role (optionnel)", required=False, max_length=20) + audit_all = discord.ui.TextInput(label="Audit global ? (oui/non)", required=False, max_length=3) async def on_submit(self, interaction: discord.Interaction): if not has_role(interaction.user, "gouvernement"): - await interaction.response.send_message("❌ Réservé au gouvernement.", ephemeral=True) + await interaction.response.send_message("Reserve au gouvernement.", ephemeral=True) return rid = None if self.role_id.value: @@ -361,31 +229,123 @@ class CreerInstitutionModal(discord.ui.Modal, title="🏛️ Créer une Institut pass can_audit = self.audit_all.value.strip().lower() in ("oui", "yes", "true", "1") try: - result = await bank_manager.creer_institution( - interaction.guild.id, self.nom.value.strip(), rid, can_audit - ) + result = await bank_manager.creer_institution(interaction.guild.id, self.nom.value.strip(), rid, can_audit) await interaction.response.send_message( - f"✅ Institution **{result['name']}** créée (ID: `{result['id']}`).", - ephemeral=True, + f"Institution **{result['name']}** creee (ID: `{result['id']}`).", ephemeral=True, ) except Exception as e: - await interaction.response.send_message(f"❌ {e}", ephemeral=True) + await interaction.response.send_message(str(e), ephemeral=True) -# ═══════════════════════════════════════════════════════════════════ -# MODALS — IDENTITE -# ═══════════════════════════════════════════════════════════════════ - -class CreerCarteModal(discord.ui.Modal, title="🪪 Créer une Carte"): - prenom = discord.ui.TextInput(label="Prénom", required=True, max_length=30) - nom_famille = discord.ui.TextInput(label="Nom de famille", required=True, max_length=30) - date_naissance = discord.ui.TextInput(label="Date de naissance", required=True, max_length=20) - sexe = discord.ui.TextInput(label="Sexe (M/F)", required=True, max_length=1) - nationalite = discord.ui.TextInput(label="Nationalité", required=True, max_length=30) +class ConfigFiscModal(discord.ui.Modal, title="Configurer le Fisc"): + institution_id = discord.ui.TextInput(label="ID de l'institution fisc", required=True, max_length=10) async def on_submit(self, interaction: discord.Interaction): if not has_role(interaction.user, "gouvernement"): - await interaction.response.send_message("❌ Réservé au gouvernement.", ephemeral=True) + await interaction.response.send_message("Reserve au gouvernement.", ephemeral=True) + return + try: + iid = int(self.institution_id.value.strip()) + except ValueError: + await interaction.response.send_message("ID invalide.", ephemeral=True) + return + ok = await bank_manager.configurer_fisc(interaction.guild.id, iid) + await interaction.response.send_message( + "Fisc configure." if ok else "Institution introuvable.", ephemeral=True, + ) + + +class GouvVirerModal(discord.ui.Modal, title="Virement Fisc"): + rib_dest = discord.ui.TextInput(label="RIB destinataire", required=True, max_length=30) + montant = discord.ui.TextInput(label="Montant (EUR)", required=True, max_length=15) + + async def on_submit(self, interaction: discord.Interaction): + if not has_role(interaction.user, "gouvernement"): + await interaction.response.send_message("Reserve au gouvernement.", ephemeral=True) + return + try: + montant = float(self.montant.value.replace(",", ".").strip()) + except ValueError: + await interaction.response.send_message("Montant invalide.", ephemeral=True) + return + rib_dest = self.rib_dest.value.strip().replace(" ", "") + try: + fisc = await bank_manager.get_fisc_config(interaction.guild.id) + if not fisc: + await interaction.response.send_message("Aucun fisc configure.", ephemeral=True) + return + source_compte = await bank_manager.get_compte_by_rib(interaction.guild.id, fisc.get("rib", "")) + if not source_compte: + await interaction.response.send_message("Compte fisc introuvable.", ephemeral=True) + return + await bank_manager.virement( + guild_id=interaction.guild.id, + rib_source=source_compte["rib"], + rib_dest=rib_dest, + montant=montant, + emetteur_id=interaction.user.id, + ) + await interaction.response.send_message(f"Virement de **{montant:.2f}EUR** effectue depuis le Fisc.", ephemeral=True) + except ValueError as e: + await interaction.response.send_message(str(e), ephemeral=True) + + +class SetPlafondModal(discord.ui.Modal, title="Plafond Negatif"): + plafond = discord.ui.TextInput(label="Plafond max (0-10000 EUR)", required=True, max_length=10) + + def __init__(self, bank_data): + super().__init__() + self.bank_data = bank_data + + async def on_submit(self, interaction: discord.Interaction): + try: + val = float(self.plafond.value.replace(",", ".").strip()) + except ValueError: + await interaction.response.send_message("Valeur invalide.", ephemeral=True) + return + if val < 0 or val > 10000: + await interaction.response.send_message("Plafond doit etre entre 0 et 10000.", ephemeral=True) + return + ok = await bank_manager.set_negative_floor(interaction.guild.id, self.bank_data["id"], val) + await interaction.response.send_message( + f"Plafond negatif de **{self.bank_data['name']}** defini a **-{val:.2f}EUR**." if ok else "Erreur.", + ephemeral=True, + ) + + +class SetReserveModal(discord.ui.Modal, title="Reserve Bancaire"): + montant = discord.ui.TextInput(label="Montant de la reserve (EUR)", required=True, max_length=15) + + def __init__(self, bank_data): + super().__init__() + self.bank_data = bank_data + + async def on_submit(self, interaction: discord.Interaction): + try: + val = float(self.montant.value.replace(",", ".").strip()) + except ValueError: + await interaction.response.send_message("Valeur invalide.", ephemeral=True) + return + if val < 0: + await interaction.response.send_message("La reserve ne peut pas etre negative.", ephemeral=True) + return + ok = await bank_manager.set_vault_balance(interaction.guild.id, self.bank_data["id"], val) + await interaction.response.send_message( + f"Reserve de **{self.bank_data['name']}** definie a **{val:.2f}EUR**." if ok else "Erreur.", + ephemeral=True, + ) + + +class CreerCarteModal(discord.ui.Modal, title="Creer une Carte"): + prenom = discord.ui.TextInput(label="Prenom", required=True, max_length=30) + nom_famille = discord.ui.TextInput(label="Nom de famille", required=True, max_length=30) + date_naissance = discord.ui.TextInput(label="Date de naissance", required=True, max_length=20) + sexe = discord.ui.TextInput(label="Sexe (M/F)", required=True, max_length=1) + nationalite = discord.ui.TextInput(label="Nationalite", required=True, max_length=30) + + async def on_submit(self, interaction: discord.Interaction): + if not has_role(interaction.user, "gouvernement"): + await interaction.response.send_message("Reserve au gouvernement.", ephemeral=True) return from utils.data_manager import DataManager dm = DataManager() @@ -399,188 +359,123 @@ class CreerCarteModal(discord.ui.Modal, title="🪪 Créer une Carte"): "nationalite": self.nationalite.value.strip(), }, ) - await interaction.response.send_message("✅ Carte d'identité créée.", ephemeral=True) + await interaction.response.send_message("Carte d'identite creee.", ephemeral=True) -class PermisModal(discord.ui.Modal, title="🚗 Permis de Conduire"): - membre_id = discord.ui.TextInput(label="ID du membre", required=True, max_length=20) - categorie = discord.ui.TextInput(label="Catégorie (A/B/C)", required=True, max_length=2) - - async def on_submit(self, interaction: discord.Interaction): - await interaction.response.send_message("✅ Permis enregistré (module à venir).", ephemeral=True) - - -class VisiteMedModal(discord.ui.Modal, title="🏥 Visite Médicale"): - membre_id = discord.ui.TextInput(label="ID du membre", required=True, max_length=20) - resultat = discord.ui.TextInput(label="Résultat (Apte/Inapte)", required=True, max_length=10) - - async def on_submit(self, interaction: discord.Interaction): - await interaction.response.send_message("✅ Visite médicale enregistrée (module à venir).", ephemeral=True) - - -# ═══════════════════════════════════════════════════════════════════ -# MODALS — SESSIONS -# ═══════════════════════════════════════════════════════════════════ - -class AnnonceSessionModal(discord.ui.Modal, title="📢 Annoncer une Session"): - date = discord.ui.TextInput( - label="Date (ex: Vendredi 20 Juin à 20h45)", required=True, max_length=100, - ) - reminder1 = discord.ui.TextInput( - label="1er rappel (ex: Jeudi 19 Juin à 19h)", required=True, max_length=100, - ) - reminder2 = discord.ui.TextInput( - label="2e rappel (ex: Vendredi 20 Juin à 15h)", required=True, max_length=100, - ) +class AnnonceSessionModal(discord.ui.Modal, title="Annoncer une Session"): + date = discord.ui.TextInput(label="Date (ex: Vendredi 20 Juin a 20h45)", required=True, max_length=100) + reminder1 = discord.ui.TextInput(label="1er rappel (ex: Jeudi 19 Juin a 19h)", required=True, max_length=100) + reminder2 = discord.ui.TextInput(label="2e rappel (ex: Vendredi 20 Juin a 15h)", required=True, max_length=100) async def on_submit(self, interaction: discord.Interaction): import dateparser from datetime import timezone - - session_date = dateparser.parse( - self.date.value, languages=["fr"], - settings={"TIMEZONE": "Europe/Paris", "RETURN_AS_TIMEZONE_AWARE": True}, - ) - r1 = dateparser.parse( - self.reminder1.value, languages=["fr"], - settings={"TIMEZONE": "Europe/Paris", "RETURN_AS_TIMEZONE_AWARE": True}, - ) - r2 = dateparser.parse( - self.reminder2.value, languages=["fr"], - settings={"TIMEZONE": "Europe/Paris", "RETURN_AS_TIMEZONE_AWARE": True}, - ) + session_date = dateparser.parse(self.date.value, languages=["fr"], settings={"TIMEZONE": "Europe/Paris", "RETURN_AS_TIMEZONE_AWARE": True}) + r1 = dateparser.parse(self.reminder1.value, languages=["fr"], settings={"TIMEZONE": "Europe/Paris", "RETURN_AS_TIMEZONE_AWARE": True}) + r2 = dateparser.parse(self.reminder2.value, languages=["fr"], settings={"TIMEZONE": "Europe/Paris", "RETURN_AS_TIMEZONE_AWARE": True}) if not session_date or not r1 or not r2: - await interaction.response.send_message("❌ Une des dates n'a pas été comprise.", ephemeral=True) + await interaction.response.send_message("Une des dates n'a pas ete comprise.", ephemeral=True) return ping_role_id = get_role_id(interaction.guild.id, "citoyen") message = ( - "# <:VV:1339237574593609780> - **SESSION VICTORIA'S VICE**\n\n" - "```📅 - HORAIRE DE LA SESSION```\n" - f"> - Nous vous donnons rendez-vous le **{self.date.value}** " - "pour une **session officielle** sur Victoria's Vice. 🗓️\n" - "---\n" - "```🤔 - COMMENT REJOINDRE LA SESSION ?```\n" - "> - Pour lancer la Map, entrez le code sur la barre de recherche de Fortnite.\n" - "> - **`CODE DE LA MAP`** <:arrow:1297491509201080362> [0986-1836-1625]\n" - "> - **`LIEN DIRECT`** <:arrow:1297491509201080362> " - "\n" - "---\n" - "```🖐 - SEREZ-VOUS PRÉSENT(E) ?```\n" - "> - Je serai présent : <:check:1297296859206778911> **(0)**\n" - "> - Je serai absent : <:uncheck:1297296882778902598> **(0)**\n" - "> - Je serai en retard : ⏰ **(0)**\n" - "---\n" - f"> - **`PING`** : <@&{ping_role_id}>" + "# - **SESSION VICTORIA'S VICE**\n\n" + f"> Rendez-vous le **{self.date.value}** pour une session officielle.\n" + f"> Ping : <@&{ping_role_id}>" ) await interaction.response.send_message(content=message) - msg = await interaction.original_response() - from commandes.annonces_sessions import PresenceView - await msg.edit(view=PresenceView(msg.id)) -class ListePresenceModal(discord.ui.Modal, title="📋 Liste des Présences"): - message_id = discord.ui.TextInput( - label="ID du message d'annonce", required=True, max_length=20 - ) +class ListePresenceModal(discord.ui.Modal, title="Liste des Presences"): + message_id = discord.ui.TextInput(label="ID du message d'annonce", required=True, max_length=20) async def on_submit(self, interaction: discord.Interaction): from commandes.annonces_sessions import presence_data try: mid = int(self.message_id.value.strip()) except ValueError: - await interaction.response.send_message("❌ ID invalide.", ephemeral=True) + await interaction.response.send_message("ID invalide.", ephemeral=True) return if mid not in presence_data: - await interaction.response.send_message("❌ Aucune donnée pour ce message.", ephemeral=True) + await interaction.response.send_message("Aucune donnee pour ce message.", ephemeral=True) return data = presence_data[mid] def mentions(ids): return ", ".join(f"<@{uid}>" for uid in ids) if ids else "Aucun" await interaction.response.send_message( - f"**Présents ({len(data['present'])})** : {mentions(data['present'])}\n" + f"**Presents ({len(data['present'])})** : {mentions(data['present'])}\n" f"**Absents ({len(data['absent'])})** : {mentions(data['absent'])}\n" f"**En retard ({len(data['retard'])})** : {mentions(data['retard'])}", ephemeral=True, ) -# ═══════════════════════════════════════════════════════════════════ -# MODALS — CONFIG -# ═══════════════════════════════════════════════════════════════════ - -class ConfigRoleModal(discord.ui.Modal, title="🎭 Définir un Rôle"): - cle = discord.ui.TextInput(label="Clé (banquier, citoyen, ems...)", required=True, max_length=30) - role_id = discord.ui.TextInput(label="ID du rôle Discord", required=True, max_length=20) +class ConfigRoleModal(discord.ui.Modal, title="Definir un Role"): + cle = discord.ui.TextInput(label="Cle (banquier, citoyen, ems...)", required=True, max_length=30) + role_id = discord.ui.TextInput(label="ID du role Discord", required=True, max_length=20) async def on_submit(self, interaction: discord.Interaction): if not interaction.user.guild_permissions.manage_guild: - await interaction.response.send_message("❌ Permission requise.", ephemeral=True) + await interaction.response.send_message("Permission requise.", ephemeral=True) return cle = self.cle.value.strip().lower() try: rid = int(self.role_id.value.strip()) except ValueError: - await interaction.response.send_message("❌ ID invalide.", ephemeral=True) + await interaction.response.send_message("ID invalide.", ephemeral=True) return role = interaction.guild.get_role(rid) if not role: - await interaction.response.send_message("❌ Rôle introuvable.", ephemeral=True) + await interaction.response.send_message("Role introuvable.", ephemeral=True) return set_config_value(interaction.guild.id, "roles", cle, rid) - await interaction.response.send_message( - f"✅ Rôle **{ROLE_LABELS.get(cle, cle)}** → {role.mention}", ephemeral=True, - ) + await interaction.response.send_message(f"Role **{ROLE_LABELS.get(cle, cle)}** -> {role.mention}", ephemeral=True) -class ConfigChannelModal(discord.ui.Modal, title="📢 Définir un Salon"): - cle = discord.ui.TextInput(label="Clé (welcome, candidature...)", required=True, max_length=30) +class ConfigChannelModal(discord.ui.Modal, title="Definir un Salon"): + cle = discord.ui.TextInput(label="Cle (welcome, candidature...)", required=True, max_length=30) channel_id = discord.ui.TextInput(label="ID du salon Discord", required=True, max_length=20) async def on_submit(self, interaction: discord.Interaction): if not interaction.user.guild_permissions.manage_guild: - await interaction.response.send_message("❌ Permission requise.", ephemeral=True) + await interaction.response.send_message("Permission requise.", ephemeral=True) return cle = self.cle.value.strip().lower() try: cid = int(self.channel_id.value.strip()) except ValueError: - await interaction.response.send_message("❌ ID invalide.", ephemeral=True) + await interaction.response.send_message("ID invalide.", ephemeral=True) return ch = interaction.guild.get_channel(cid) if not ch: - await interaction.response.send_message("❌ Salon introuvable.", ephemeral=True) + await interaction.response.send_message("Salon introuvable.", ephemeral=True) return set_config_value(interaction.guild.id, "channels", cle, cid) - await interaction.response.send_message( - f"✅ Salon **{CHANNEL_LABELS.get(cle, cle)}** → {ch.mention}", ephemeral=True, - ) + await interaction.response.send_message(f"Salon **{CHANNEL_LABELS.get(cle, cle)}** -> {ch.mention}", ephemeral=True) -class ConfigCategoryModal(discord.ui.Modal, title="📁 Définir une Catégorie"): - cle = discord.ui.TextInput(label="Clé (ticket)", required=True, max_length=30) - category_id = discord.ui.TextInput(label="ID de la catégorie Discord", required=True, max_length=20) +class ConfigCategoryModal(discord.ui.Modal, title="Definir une Categorie"): + cle = discord.ui.TextInput(label="Cle (ticket)", required=True, max_length=30) + category_id = discord.ui.TextInput(label="ID de la categorie Discord", required=True, max_length=20) async def on_submit(self, interaction: discord.Interaction): if not interaction.user.guild_permissions.manage_guild: - await interaction.response.send_message("❌ Permission requise.", ephemeral=True) + await interaction.response.send_message("Permission requise.", ephemeral=True) return cle = self.cle.value.strip().lower() try: cid = int(self.category_id.value.strip()) except ValueError: - await interaction.response.send_message("❌ ID invalide.", ephemeral=True) + await interaction.response.send_message("ID invalide.", ephemeral=True) return cat = interaction.guild.get_channel(cid) if not cat: - await interaction.response.send_message("❌ Catégorie introuvable.", ephemeral=True) + await interaction.response.send_message("Categorie introuvable.", ephemeral=True) return set_config_value(interaction.guild.id, "categories", cle, cid) - await interaction.response.send_message( - f"✅ Catégorie **{CATEGORY_LABELS.get(cle, cle)}** → **{cat.name}**", ephemeral=True, - ) + await interaction.response.send_message(f"Categorie **{CATEGORY_LABELS.get(cle, cle)}** -> **{cat.name}**", ephemeral=True) -class RollModal(discord.ui.Modal, title="🎲 Lancer un Dé"): +class RollModal(discord.ui.Modal, title="Lancer un De"): minimum = discord.ui.TextInput(label="Valeur minimale", required=True, max_length=5) maximum = discord.ui.TextInput(label="Valeur maximale", required=True, max_length=5) raison = discord.ui.TextInput(label="Raison", required=True, max_length=100) @@ -590,21 +485,21 @@ class RollModal(discord.ui.Modal, title="🎲 Lancer un Dé"): min_val = int(self.minimum.value.strip()) max_val = int(self.maximum.value.strip()) except ValueError: - await interaction.response.send_message("❌ Valeurs invalides.", ephemeral=True) + await interaction.response.send_message("Valeurs invalides.", ephemeral=True) return if min_val > max_val: - await interaction.response.send_message("❌ Min > Max.", ephemeral=True) + await interaction.response.send_message("Min > Max.", ephemeral=True) return result = random.randint(min_val, max_val) embed = discord.Embed( - title="🎲 Résultat du dé", + title="Resultat du de", description=f"**{result}** (entre {min_val} et {max_val})\n*Raison : {self.raison.value.strip()}*", color=C_FUN, ) await interaction.response.send_message(embed=embed) -class MessageModal(discord.ui.Modal, title="✉️ Envoyer un MP"): +class MessageModal(discord.ui.Modal, title="Envoyer un MP"): user_id = discord.ui.TextInput(label="ID de l'utilisateur", required=True, max_length=20) message = discord.ui.TextInput(label="Message", required=True, style=discord.TextStyle.paragraph, max_length=2000) @@ -612,27 +507,27 @@ class MessageModal(discord.ui.Modal, title="✉️ Envoyer un MP"): try: uid = int(self.user_id.value.strip()) except ValueError: - await interaction.response.send_message("❌ ID invalide.", ephemeral=True) + await interaction.response.send_message("ID invalide.", ephemeral=True) return user = interaction.bot.get_user(uid) if not user: - await interaction.response.send_message("❌ Utilisateur introuvable.", ephemeral=True) + await interaction.response.send_message("Utilisateur introuvable.", ephemeral=True) return try: await user.send(self.message.value) - await interaction.response.send_message(f"✅ MP envoyé à {user.mention}.", ephemeral=True) + await interaction.response.send_message(f"MP envoye a {user.mention}.", ephemeral=True) except discord.Forbidden: - await interaction.response.send_message("❌ Impossible d'envoyer un MP.", ephemeral=True) + await interaction.response.send_message("Impossible d'envoyer un MP.", ephemeral=True) -class MessageTousModal(discord.ui.Modal, title="📢 MP à Tous"): +class MessageTousModal(discord.ui.Modal, title="MP a Tous"): message = discord.ui.TextInput(label="Message", required=True, style=discord.TextStyle.paragraph, max_length=2000) async def on_submit(self, interaction: discord.Interaction): if not interaction.user.guild_permissions.administrator: - await interaction.response.send_message("❌ Admin uniquement.", ephemeral=True) + await interaction.response.send_message("Admin uniquement.", ephemeral=True) return - await interaction.response.send_message("📨 Envoi en cours...", ephemeral=True) + await interaction.response.send_message("Envoi en cours...", ephemeral=True) count = 0 for member in interaction.guild.members: if not member.bot: @@ -641,11 +536,11 @@ class MessageTousModal(discord.ui.Modal, title="📢 MP à Tous"): count += 1 except discord.Forbidden: pass - await interaction.followup.send(f"✅ MP envoyé à **{count}** membres.", ephemeral=True) + await interaction.followup.send(f"MP envoye a **{count}** membres.", ephemeral=True) # ═══════════════════════════════════════════════════════════════════ -# VIEWS — MES COMPTES (tout le monde) +# VIEWS — COMPTES # ═══════════════════════════════════════════════════════════════════ class CompteDetailView(discord.ui.LayoutView): @@ -661,14 +556,15 @@ class CompteDetailView(discord.ui.LayoutView): acc_type = compte_data["account_type"] tva = compte_data.get("tva_rate", 0) bank_name = bank_data["name"] + safe = _uid(rib) solde_color = discord.Color.green() if solde >= 0 else discord.Color.red() items = [ - discord.ui.TextDisplay(f"## 💳 {bank_name} — {acc_type.title()}"), + discord.ui.TextDisplay(f"## {bank_name} — {acc_type.title()}"), discord.ui.Separator(), discord.ui.TextDisplay(f"**RIB :** `{rib}`"), - discord.ui.TextDisplay(f"**Solde :** `{solde:,.2f}€`".replace(",", " ")), + discord.ui.TextDisplay(f"**Solde :** `{solde:,.2f} EUR`".replace(",", " ")), ] if acc_type == "entreprise" and tva > 0: items.append(discord.ui.TextDisplay(f"**TVA :** `{tva}%`")) @@ -676,10 +572,13 @@ class CompteDetailView(discord.ui.LayoutView): items.extend([ discord.ui.Separator(), discord.ui.ActionRow( - _btn("💳 Carte", discord.ButtonStyle.primary, f"cd_card_{rib[-8:]}", self._card), - _btn("📤 Virement", discord.ButtonStyle.primary, f"cd_vir_{rib[-8:]}", self._vir), - _btn("➕ Dépôt", discord.ButtonStyle.success, f"cd_dep_{rib[-8:]}", self._depot), - _btn("➖ Retrait", discord.ButtonStyle.danger, f"cd_ret_{rib[-8:]}", self._retrait), + _btn("Carte", discord.ButtonStyle.primary, f"cd_card_{safe}", self._card), + _btn("Releve", discord.ButtonStyle.primary, f"cd_stmt_{safe}", self._statement), + _btn("Virement", discord.ButtonStyle.primary, f"cd_vir_{safe}", self._vir), + ), + discord.ui.ActionRow( + _btn("+ Depot", discord.ButtonStyle.success, f"cd_dep_{safe}", self._depot), + _btn("- Retrait", discord.ButtonStyle.danger, f"cd_ret_{safe}", self._retrait), ), discord.ui.Separator(), discord.ui.ActionRow(_back_btn("back_cd", self._back)), @@ -703,25 +602,36 @@ class CompteDetailView(discord.ui.LayoutView): embed.set_image(url="attachment://carte_bancaire.png") await interaction.response.send_message(file=file, embed=embed, ephemeral=True) - async def _vir(self, interaction: discord.Interaction): - await interaction.response.send_modal( - VirementModal(source_rib=self.compte["rib"]) + async def _statement(self, interaction: discord.Interaction): + txs = await bank_manager.get_transactions(interaction.guild.id, self.compte["rib"], limit=20) + from utils.bank_statement import BankStatementGenerator + gen = BankStatementGenerator() + buf = gen.generate( + bank_name=self.bank["name"], + holder_name=interaction.user.display_name, + rib=self.compte["rib"], + solde_actuel=self.compte["solde"], + transactions=txs, + account_type=self.compte["account_type"], ) + file = discord.File(buf, filename="releve_bancaire.png") + embed = discord.Embed(title="Releve bancaire", color=C_BANK) + embed.set_image(url="attachment://releve_bancaire.png") + await interaction.response.send_message(file=file, embed=embed, ephemeral=True) + + async def _vir(self, interaction: discord.Interaction): + await interaction.response.send_modal(VirementModal(source_rib=self.compte["rib"])) async def _depot(self, interaction: discord.Interaction): - await interaction.response.send_modal( - DepotRetraitModal(rib=self.compte["rib"], operation="depot") - ) + await interaction.response.send_modal(DepotRetraitModal(rib=self.compte["rib"], operation="depot")) async def _retrait(self, interaction: discord.Interaction): - await interaction.response.send_modal( - DepotRetraitModal(rib=self.compte["rib"], operation="retrait") - ) + await interaction.response.send_modal(DepotRetraitModal(rib=self.compte["rib"], operation="retrait")) async def _back(self, interaction: discord.Interaction): - await interaction.response.edit_message( - view=ComptePanelView(interaction.user, self.bot_ref) - ) + view = ComptePanelView(interaction.user, self.bot_ref) + await view._load_accounts(interaction) + await interaction.response.edit_message(view=view) class ComptePanelView(discord.ui.LayoutView): @@ -730,20 +640,12 @@ class ComptePanelView(discord.ui.LayoutView): self.author = author self.bot_ref = bot - items = [ - discord.ui.TextDisplay("## 💰 Mes Comptes"), - discord.ui.TextDisplay("Cliquez sur un compte pour voir les détails et opérer."), - discord.ui.Separator(), - ] - - self._account_buttons = [] - async def _load_accounts(self, interaction: discord.Interaction): - comptes = await bank_manager.get_comptes_user(interaction.guild.id, interaction.user.id) + comptes = await bank_manager.get_comptes_user(interaction.guild.id, self.author.id) if not comptes: items = [ - discord.ui.TextDisplay("## 💰 Mes Comptes"), - discord.ui.TextDisplay("Aucun compte bancaire. contactez un banquier pour en ouvrir un."), + discord.ui.TextDisplay("## Mes Comptes"), + discord.ui.TextDisplay("Aucun compte bancaire. Contactez un banquier."), discord.ui.Separator(), discord.ui.ActionRow(_back_btn("back_cp", self._back)), ] @@ -751,20 +653,16 @@ class ComptePanelView(discord.ui.LayoutView): return items = [ - discord.ui.TextDisplay("## 💰 Mes Comptes"), - discord.ui.TextDisplay(f"**{len(comptes)}** compte(s) trouvé(s). Cliquez pour details."), + discord.ui.TextDisplay("## Mes Comptes"), + discord.ui.TextDisplay(f"**{len(comptes)}** compte(s). Cliquez pour details."), discord.ui.Separator(), ] - for i, c in enumerate(comptes): - solde_str = f"{c['solde']:,.2f}€".replace(",", " ") - label = f"💳 {c['bank_name']} — {c['account_type'].title()} — {solde_str}" - btn = _btn( - label[:80], - discord.ButtonStyle.primary, - f"cp_acc_{i}", - self._make_acc_cb(c), - ) + for c in comptes: + solde_str = f"{c['solde']:,.2f} EUR".replace(",", " ") + label = f"{c['bank_name']} — {c['account_type'].title()} — {solde_str}" + safe = _uid(c["rib"]) + btn = _btn(label[:80], discord.ButtonStyle.primary, f"cp_acc_{safe}", self._make_acc_cb(c)) items.append(discord.ui.ActionRow(btn)) items.extend([ @@ -781,14 +679,12 @@ class ComptePanelView(discord.ui.LayoutView): ) return cb - async def _back(self, interaction: discord.Interaction): - await interaction.response.edit_message( - view=MainPanelView(interaction.user, self.bot_ref) - ) + async def _back(self, interaction): + await interaction.response.edit_message(view=MainPanelView(interaction.user, self.bot_ref)) # ═══════════════════════════════════════════════════════════════════ -# VIEWS — OPERATIONS BANQUIER +# VIEWS — BANQUIER # ═══════════════════════════════════════════════════════════════════ class BanquierPanelView(discord.ui.LayoutView): @@ -797,17 +693,29 @@ class BanquierPanelView(discord.ui.LayoutView): self.author = author self.bot_ref = bot self.selected_user = None - self.user_select = TargetUserSelect(custom_id="bp_usel") items = [ - discord.ui.TextDisplay("## 🏦 Opérations Banquier"), - discord.ui.TextDisplay("Sélectionnez un membre puis choisissez une opération."), - discord.ui.Separator(), - discord.ui.ActionRow(self.user_select), + discord.ui.TextDisplay("## Operations Banquier"), + discord.ui.TextDisplay("Selectionnez un membre, puis choisissez une operation."), discord.ui.Separator(), discord.ui.ActionRow( - _btn("🏦 Ouvrir Compte", discord.ButtonStyle.success, "bp_ouvrir", self._ouvrir), - _btn("📜 Transactions", discord.ButtonStyle.secondary, "bp_tx", self._tx), + discord.ui.UserSelect( + placeholder="Selectionner un membre...", + custom_id=_uid("bq_sel_" + str(author.id)), + ) + ), + discord.ui.Separator(), + discord.ui.ActionRow( + _btn("Ouvrir Compte", discord.ButtonStyle.success, _uid("bq_open_" + str(author.id)), self._ouvrir), + _btn("Consulter Clients", discord.ButtonStyle.secondary, _uid("bq_clients_" + str(author.id)), self._clients), + ), + discord.ui.ActionRow( + _btn("Info Banque", discord.ButtonStyle.secondary, _uid("bq_info_" + str(author.id)), self._info), + _btn("Transactions", discord.ButtonStyle.secondary, _uid("bq_tx_" + str(author.id)), self._tx), + ), + discord.ui.ActionRow( + _btn("Set Plafond", discord.ButtonStyle.primary, _uid("bq_plaf_" + str(author.id)), self._plafond), + _btn("Set Reserve", discord.ButtonStyle.primary, _uid("bq_res_" + str(author.id)), self._reserve), ), discord.ui.Separator(), discord.ui.ActionRow(_back_btn("back_bp", self._back)), @@ -815,19 +723,104 @@ class BanquierPanelView(discord.ui.LayoutView): self.add_item(discord.ui.Container(*items, accent_colour=discord.Colour(C_BANQ_OP))) - def _target(self, interaction): - return self.user_select.selected_user or interaction.user + for child in self.children: + if isinstance(child, discord.ui.Container): + for item in child.children: + if isinstance(item, discord.ui.ActionRow): + for sub in item.children: + if isinstance(sub, discord.ui.UserSelect): + sub.callback = self._user_selected + break + + async def _user_selected(self, interaction: discord.Interaction): + self.selected_user = interaction.data["values"][0] + await interaction.response.defer() async def _back(self, interaction): - await interaction.response.edit_message( - view=MainPanelView(interaction.user, self.bot_ref) - ) + await interaction.response.edit_message(view=MainPanelView(interaction.user, self.bot_ref)) async def _ouvrir(self, interaction): - await interaction.response.send_modal(OuvrirCompteModal()) + bank = await _get_user_bank(interaction.guild, interaction.user) + await interaction.response.send_modal(OuvrirCompteModal(bank_data=bank)) + + async def _clients(self, interaction): + bank = await _get_user_bank(interaction.guild, interaction.user) + if not bank: + await interaction.response.send_message("Aucune banque associee.", ephemeral=True) + return + comptes = await bank_manager.get_comptes_banque(interaction.guild.id, bank["id"]) + if not comptes: + await interaction.response.send_message("Aucun compte dans cette banque.", ephemeral=True) + return + embed = discord.Embed(title=f"Clients de {bank['name']}", color=discord.Color.blue()) + for c in comptes: + member = interaction.guild.get_member(c["user_id"]) + name = member.display_name if member else f"<@{c['user_id']}>" + tva_text = f" | TVA: {c['tva_rate']}%" if c["account_type"] == "entreprise" else "" + embed.add_field( + name=f"{name} ({c['account_type']})", + value=f"**RIB :** `{c['rib']}`\n**Solde :** `{c['solde']:.2f}EUR`{tva_text}", + inline=False, + ) + embed.set_footer(text=f"{len(comptes)} compte(s)") + await interaction.response.send_message(embed=embed, ephemeral=True) + + async def _info(self, interaction): + bank = await _get_user_bank(interaction.guild, interaction.user) + if not bank: + await interaction.response.send_message("Aucune banque associee.", ephemeral=True) + return + role = interaction.guild.get_role(bank["role_id"]) + comptes = await bank_manager.get_comptes_banque(interaction.guild.id, bank["id"]) + nb_p = sum(1 for c in comptes if c["account_type"] == "particulier") + nb_e = sum(1 for c in comptes if c["account_type"] == "entreprise") + embed = discord.Embed(title=f"{bank['name']}", color=discord.Color.blue()) + embed.add_field(name="Role", value=role.mention if role else "Inconnu", inline=True) + embed.add_field(name="Code etabl.", value=f"`{bank['establishment_code']}`", inline=True) + embed.add_field(name="Reserve", value=f"`{bank['vault_balance']:.2f}EUR`", inline=True) + embed.add_field(name="Encours clients", value=f"`{bank['clients_total_balance']:.2f}EUR`", inline=True) + embed.add_field(name="Particuliers", value=str(nb_p), inline=True) + embed.add_field(name="Entreprises", value=str(nb_e), inline=True) + if bank.get("client_negative_floor", 0) > 0: + embed.add_field(name="Plafond neg.", value=f"`-{bank['client_negative_floor']:.2f}EUR`", inline=True) + await interaction.response.send_message(embed=embed, ephemeral=True) async def _tx(self, interaction): - await interaction.response.send_modal(TransactionsModal()) + if not self.selected_user: + await interaction.response.send_message("Selectionnez d'abord un membre.", ephemeral=True) + return + uid = int(self.selected_user) if isinstance(self.selected_user, (int, str)) else self.selected_user.id + comptes = await bank_manager.get_comptes_user(interaction.guild.id, uid) + if not comptes: + await interaction.response.send_message("Aucun compte pour ce membre.", ephemeral=True) + return + member = interaction.guild.get_member(uid) + name = member.display_name if member else str(uid) + embed = discord.Embed(title=f"Transactions de {name}", color=discord.Color.blue()) + for c in comptes: + txs = await bank_manager.get_transactions(interaction.guild.id, c["rib"], limit=5) + if txs: + lines = [] + for tx in txs: + is_out = tx["emetteur_rib"] == c["rib"] + signe = "-" if is_out else "+" + lines.append(f"`{tx['date'][:16]}` **{tx['type']}** `{signe}{tx['montant']:.2f}EUR`") + embed.add_field(name=f"{c['bank_name']} ({c['account_type']})", value="\n".join(lines), inline=False) + await interaction.response.send_message(embed=embed, ephemeral=True) + + async def _plafond(self, interaction): + bank = await _get_user_bank(interaction.guild, interaction.user) + if not bank: + await interaction.response.send_message("Aucune banque associee.", ephemeral=True) + return + await interaction.response.send_modal(SetPlafondModal(bank)) + + async def _reserve(self, interaction): + bank = await _get_user_bank(interaction.guild, interaction.user) + if not bank: + await interaction.response.send_message("Aucune banque associee.", ephemeral=True) + return + await interaction.response.send_modal(SetReserveModal(bank)) # ═══════════════════════════════════════════════════════════════════ @@ -841,20 +834,20 @@ class GouvernementPanelView(discord.ui.LayoutView): self.bot_ref = bot items = [ - discord.ui.TextDisplay("## 🏛️ Gouvernement"), - discord.ui.TextDisplay("Institutions, fisc, banques nationales."), + discord.ui.TextDisplay("## Gouvernement"), + discord.ui.TextDisplay("Institutions, fisc, banques."), discord.ui.Separator(), discord.ui.ActionRow( - _btn("🏦 Créer Banque", discord.ButtonStyle.success, "gp_cbanque", self._cbanque), - _btn("🏛️ Créer Institution", discord.ButtonStyle.success, "gp_cinst", self._cinst), + _btn("Creer Banque", discord.ButtonStyle.success, "gp_cbanque", self._cbanque), + _btn("Creer Institution", discord.ButtonStyle.success, "gp_cinst", self._cinst), ), discord.ui.ActionRow( - _btn("⚙️ Config Fisc", discord.ButtonStyle.primary, "gp_fisc", self._fisc), - _btn("💸 Virement Fisc", discord.ButtonStyle.primary, "gp_vfisc", self._vfisc), + _btn("Config Fisc", discord.ButtonStyle.primary, "gp_fisc", self._fisc), + _btn("Virement Fisc", discord.ButtonStyle.primary, "gp_vfisc", self._vfisc), ), discord.ui.ActionRow( - _btn("📋 Lister Banques", discord.ButtonStyle.secondary, "gp_lbanques", self._lbanques), - _btn("📋 Lister Institutions", discord.ButtonStyle.secondary, "gp_linst", self._linst), + _btn("Lister Banques", discord.ButtonStyle.secondary, "gp_lbanques", self._lbanques), + _btn("Lister Institutions", discord.ButtonStyle.secondary, "gp_linst", self._linst), ), discord.ui.Separator(), discord.ui.ActionRow(_back_btn("back_gp", self._back)), @@ -863,52 +856,50 @@ class GouvernementPanelView(discord.ui.LayoutView): self.add_item(discord.ui.Container(*items, accent_colour=discord.Colour(C_GOV))) async def _back(self, interaction): - await interaction.response.edit_message( - view=MainPanelView(interaction.user, self.bot_ref) - ) + await interaction.response.edit_message(view=MainPanelView(interaction.user, self.bot_ref)) async def _cbanque(self, interaction): if not has_role(interaction.user, "gouvernement"): - await interaction.response.send_message("❌ Réservé au gouvernement.", ephemeral=True) + await interaction.response.send_message("Reserve au gouvernement.", ephemeral=True) return await interaction.response.send_modal(CreerBanqueModal()) async def _cinst(self, interaction): if not has_role(interaction.user, "gouvernement"): - await interaction.response.send_message("❌ Réservé au gouvernement.", ephemeral=True) + await interaction.response.send_message("Reserve au gouvernement.", ephemeral=True) return await interaction.response.send_modal(CreerInstitutionModal()) async def _fisc(self, interaction): if not has_role(interaction.user, "gouvernement"): - await interaction.response.send_message("❌ Réservé au gouvernement.", ephemeral=True) + await interaction.response.send_message("Reserve au gouvernement.", ephemeral=True) return await interaction.response.send_modal(ConfigFiscModal()) async def _vfisc(self, interaction): if not has_role(interaction.user, "gouvernement"): - await interaction.response.send_message("❌ Réservé au gouvernement.", ephemeral=True) + await interaction.response.send_message("Reserve au gouvernement.", ephemeral=True) return await interaction.response.send_modal(GouvVirerModal()) async def _lbanques(self, interaction): if not has_role(interaction.user, "gouvernement"): - await interaction.response.send_message("❌ Réservé au gouvernement.", ephemeral=True) + await interaction.response.send_message("Reserve au gouvernement.", ephemeral=True) return banques = await bank_manager.lister_banques(interaction.guild.id) if not banques: await interaction.response.send_message("Aucune banque.", ephemeral=True) return - embed = discord.Embed(title="🏦 Banques", color=discord.Color.gold()) + embed = discord.Embed(title="Banques", color=discord.Color.gold()) for b in banques: role = interaction.guild.get_role(b["role_id"]) embed.add_field( name=b["name"], value=( - f"**Rôle :** {role.mention if role else '❌'}\n" + f"**Role :** {role.mention if role else 'N/A'}\n" f"**Code :** `{b['establishment_code']}`\n" - f"**Réserve :** `{b['vault_balance']:.2f}€`\n" - f"**Encours :** `{b['clients_total_balance']:.2f}€`" + f"**Reserve :** `{b['vault_balance']:.2f}EUR`\n" + f"**Encours :** `{b['clients_total_balance']:.2f}EUR`" ), inline=False, ) @@ -916,20 +907,20 @@ class GouvernementPanelView(discord.ui.LayoutView): async def _linst(self, interaction): if not has_role(interaction.user, "gouvernement"): - await interaction.response.send_message("❌ Réservé au gouvernement.", ephemeral=True) + await interaction.response.send_message("Reserve au gouvernement.", ephemeral=True) return insts = await bank_manager.lister_institutions(interaction.guild.id) if not insts: await interaction.response.send_message("Aucune institution.", ephemeral=True) return - embed = discord.Embed(title="🏛️ Institutions", color=discord.Color.gold()) + embed = discord.Embed(title="Institutions", color=discord.Color.gold()) for i in insts: role = interaction.guild.get_role(i["role_id"]) if i["role_id"] else None embed.add_field( name=i["name"], value=( f"**ID :** `{i['id']}`\n" - f"**Rôle :** {role.mention if role else 'Non défini'}\n" + f"**Role :** {role.mention if role else 'Non defini'}\n" f"**Audit global :** {'Oui' if i['can_audit_all'] else 'Non'}" ), inline=False, @@ -948,26 +939,24 @@ class IdentitePanelView(discord.ui.LayoutView): self.bot_ref = bot items = [ - discord.ui.TextDisplay("## 🪪 Identité"), - discord.ui.TextDisplay("Cartes d'identité, permis, visites médicales."), + discord.ui.TextDisplay("## Identite"), + discord.ui.TextDisplay("Cartes d'identite, permis, visites medicales."), discord.ui.Separator(), discord.ui.ActionRow( - _btn("🪪 Ma Carte", discord.ButtonStyle.primary, "ip_carte", self._carte), + _btn("Ma Carte", discord.ButtonStyle.primary, "ip_carte", self._carte), ), ] if has_role(author, "gouvernement"): - items.append( - discord.ui.ActionRow( - _btn("🪪 Créer Carte", discord.ButtonStyle.success, "ip_ccar", self._ccar), - ) - ) + items.append(discord.ui.ActionRow( + _btn("Creer Carte", discord.ButtonStyle.success, "ip_ccar", self._ccar), + )) action_items = [] if has_role(author, "auto_ecole"): - action_items.append(_btn("🚗 Permis", discord.ButtonStyle.primary, "ip_permis", self._permis)) + action_items.append(_btn("Permis", discord.ButtonStyle.primary, "ip_permis", self._permis)) if has_role(author, "ems"): - action_items.append(_btn("🏥 Visite Médicale", discord.ButtonStyle.primary, "ip_vismed", self._vismed)) + action_items.append(_btn("Visite Medicale", discord.ButtonStyle.primary, "ip_vismed", self._vismed)) if action_items: items.append(discord.ui.ActionRow(*action_items)) @@ -979,9 +968,7 @@ class IdentitePanelView(discord.ui.LayoutView): self.add_item(discord.ui.Container(*items, accent_colour=discord.Colour(C_ID))) async def _back(self, interaction): - await interaction.response.edit_message( - view=MainPanelView(interaction.user, self.bot_ref) - ) + await interaction.response.edit_message(view=MainPanelView(interaction.user, self.bot_ref)) async def _carte(self, interaction): from utils.data_manager import DataManager @@ -989,27 +976,27 @@ class IdentitePanelView(discord.ui.LayoutView): dm = DataManager() identity = dm.identities.get(uid) if not identity: - await interaction.response.send_message("❌ Aucune carte trouvée.", ephemeral=True) + await interaction.response.send_message("Aucune carte trouvée.", ephemeral=True) return try: from utils.card_generator import CardGenerator cg = CardGenerator() card_buffer = await cg.create_identity_card(interaction.user, identity) file = discord.File(card_buffer, filename="carte_identite.png") - embed = discord.Embed(title="🪪 Carte d'identité", color=C_ID) + embed = discord.Embed(title="Carte d'identite", color=C_ID) embed.set_image(url="attachment://carte_identite.png") await interaction.response.send_message(file=file, embed=embed) except Exception as e: - await interaction.response.send_message(f"❌ Erreur : {e}", ephemeral=True) + await interaction.response.send_message(f"Erreur : {e}", ephemeral=True) async def _ccar(self, interaction): await interaction.response.send_modal(CreerCarteModal()) async def _permis(self, interaction): - await interaction.response.send_modal(PermisModal()) + await interaction.response.send_message("Module permis a venir.", ephemeral=True) async def _vismed(self, interaction): - await interaction.response.send_modal(VisiteMedModal()) + await interaction.response.send_message("Module visite medicale a venir.", ephemeral=True) # ═══════════════════════════════════════════════════════════════════ @@ -1023,13 +1010,13 @@ class TicketsPanelView(discord.ui.LayoutView): self.bot_ref = bot items = [ - discord.ui.TextDisplay("## 📩 Tickets & Messages"), - discord.ui.TextDisplay("Gestion des tickets et messages privés."), + discord.ui.TextDisplay("## Tickets & Messages"), + discord.ui.TextDisplay("Gestion des tickets et messages prives."), discord.ui.Separator(), discord.ui.ActionRow( - _btn("📑 Embed Ticket", discord.ButtonStyle.success, "tp_ticket", self._ticket), - _btn("✉️ Envoyer MP", discord.ButtonStyle.primary, "tp_mp", self._mp), - _btn("📢 MP à Tous", discord.ButtonStyle.danger, "tp_mptous", self._mptous), + _btn("Embed Ticket", discord.ButtonStyle.success, "tp_ticket", self._ticket), + _btn("Envoyer MP", discord.ButtonStyle.primary, "tp_mp", self._mp), + _btn("MP a Tous", discord.ButtonStyle.danger, "tp_mptous", self._mptous), ), discord.ui.Separator(), discord.ui.ActionRow(_back_btn("back_tp", self._back)), @@ -1038,22 +1025,20 @@ class TicketsPanelView(discord.ui.LayoutView): self.add_item(discord.ui.Container(*items, accent_colour=discord.Colour(C_TICKET))) async def _back(self, interaction): - await interaction.response.edit_message( - view=MainPanelView(interaction.user, self.bot_ref) - ) + await interaction.response.edit_message(view=MainPanelView(interaction.user, self.bot_ref)) async def _ticket(self, interaction): if not interaction.user.guild_permissions.manage_guild: - await interaction.response.send_message("❌ Permission requise.", ephemeral=True) + await interaction.response.send_message("Permission requise.", ephemeral=True) return embed = discord.Embed( - title="📩 Ticket Whitelist", - description="Pour réaliser votre whitelist, ouvrez un ticket et remplissez le formulaire.", + title="Ticket Whitelist", + description="Ouvrez un ticket et remplissez le formulaire.", color=0x00FF00, ) from commandes.ticket import TicketView await interaction.channel.send(embed=embed, view=TicketView()) - await interaction.response.send_message("✅ Embed des tickets envoyé.", ephemeral=True) + await interaction.response.send_message("Embed des tickets envoye.", ephemeral=True) async def _mp(self, interaction): await interaction.response.send_modal(MessageModal()) @@ -1063,7 +1048,7 @@ class TicketsPanelView(discord.ui.LayoutView): # ═══════════════════════════════════════════════════════════════════ -# VIEWS — SESSIONS RP +# VIEWS — SESSIONS # ═══════════════════════════════════════════════════════════════════ class SessionsPanelView(discord.ui.LayoutView): @@ -1073,12 +1058,12 @@ class SessionsPanelView(discord.ui.LayoutView): self.bot_ref = bot items = [ - discord.ui.TextDisplay("## 📅 Sessions RP"), - discord.ui.TextDisplay("Annonces de sessions et gestion des présences."), + discord.ui.TextDisplay("## Sessions RP"), + discord.ui.TextDisplay("Annonces de sessions et gestion des presences."), discord.ui.Separator(), discord.ui.ActionRow( - _btn("📢 Annoncer Session", discord.ButtonStyle.success, "sp_annonce", self._annonce), - _btn("📋 Liste Présences", discord.ButtonStyle.primary, "sp_liste", self._liste), + _btn("Annoncer Session", discord.ButtonStyle.success, "sp_annonce", self._annonce), + _btn("Liste Presences", discord.ButtonStyle.primary, "sp_liste", self._liste), ), discord.ui.Separator(), discord.ui.ActionRow(_back_btn("back_sp", self._back)), @@ -1087,9 +1072,7 @@ class SessionsPanelView(discord.ui.LayoutView): self.add_item(discord.ui.Container(*items, accent_colour=discord.Colour(C_SESSION))) async def _back(self, interaction): - await interaction.response.edit_message( - view=MainPanelView(interaction.user, self.bot_ref) - ) + await interaction.response.edit_message(view=MainPanelView(interaction.user, self.bot_ref)) async def _annonce(self, interaction): await interaction.response.send_modal(AnnonceSessionModal()) @@ -1109,17 +1092,17 @@ class ConfigPanelView(discord.ui.LayoutView): self.bot_ref = bot items = [ - discord.ui.TextDisplay("## ⚙️ Configuration"), - discord.ui.TextDisplay("Configurer les rôles, salons et catégories de ce serveur."), + discord.ui.TextDisplay("## Configuration"), + discord.ui.TextDisplay("Configurer les roles, salons et categories."), discord.ui.Separator(), discord.ui.ActionRow( - _btn("🎭 Définir Rôle", discord.ButtonStyle.primary, "cfg_role", self._crole), - _btn("📢 Définir Salon", discord.ButtonStyle.primary, "cfg_chan", self._cchan), - _btn("📁 Définir Catégorie", discord.ButtonStyle.primary, "cfg_cat", self._ccat), + _btn("Definir Role", discord.ButtonStyle.primary, "cfg_role", self._crole), + _btn("Definir Salon", discord.ButtonStyle.primary, "cfg_chan", self._cchan), + _btn("Definir Categorie", discord.ButtonStyle.primary, "cfg_cat", self._ccat), ), discord.ui.ActionRow( - _btn("👁️ Voir Config", discord.ButtonStyle.secondary, "cfg_voir", self._cvoir), - _btn("🗑️ Reset Config", discord.ButtonStyle.danger, "cfg_reset", self._creset), + _btn("Voir Config", discord.ButtonStyle.secondary, "cfg_voir", self._cvoir), + _btn("Reset Config", discord.ButtonStyle.danger, "cfg_reset", self._creset), ), discord.ui.Separator(), discord.ui.ActionRow(_back_btn("back_cfg", self._back)), @@ -1128,9 +1111,7 @@ class ConfigPanelView(discord.ui.LayoutView): self.add_item(discord.ui.Container(*items, accent_colour=discord.Colour(C_CONFIG))) async def _back(self, interaction): - await interaction.response.edit_message( - view=MainPanelView(interaction.user, self.bot_ref) - ) + await interaction.response.edit_message(view=MainPanelView(interaction.user, self.bot_ref)) async def _crole(self, interaction): await interaction.response.send_modal(ConfigRoleModal()) @@ -1143,45 +1124,45 @@ class ConfigPanelView(discord.ui.LayoutView): async def _cvoir(self, interaction): if not interaction.user.guild_permissions.manage_guild: - await interaction.response.send_message("❌ Permission requise.", ephemeral=True) + await interaction.response.send_message("Permission requise.", ephemeral=True) return config = load_guild_config(interaction.guild.id) - embed = discord.Embed(title="⚙️ Configuration actuelle", color=C_CONFIG) + embed = discord.Embed(title="Configuration actuelle", color=C_CONFIG) roles_text = "" for key, label in ROLE_LABELS.items(): rid = config.get("roles", {}).get(key) if rid: role = interaction.guild.get_role(int(rid)) - roles_text += f"> {label} : {role.mention if role else f'❌ {rid}'}\n" + roles_text += f"> {label} : {role.mention if role else f'N/A {rid}'}\n" else: - roles_text += f"> {label} : ⚠️ Non défini\n" - embed.add_field(name="🎭 Rôles", value=roles_text, inline=False) + roles_text += f"> {label} : Non defini\n" + embed.add_field(name="Roles", value=roles_text, inline=False) channels_text = "" for key, label in CHANNEL_LABELS.items(): cid = config.get("channels", {}).get(key) if cid: ch = interaction.guild.get_channel(int(cid)) - channels_text += f"> {label} : {ch.mention if ch else f'❌ {cid}'}\n" + channels_text += f"> {label} : {ch.mention if ch else f'N/A {cid}'}\n" else: - channels_text += f"> {label} : ⚠️ Non défini\n" - embed.add_field(name="📢 Salons", value=channels_text, inline=False) + channels_text += f"> {label} : Non defini\n" + embed.add_field(name="Salons", value=channels_text, inline=False) cats_text = "" for key, label in CATEGORY_LABELS.items(): catid = config.get("categories", {}).get(key) if catid: cat = interaction.guild.get_channel(int(catid)) - cats_text += f"> {label} : **{cat.name}**\n" if cat else f"> {label} : ❌ {catid}\n" + cats_text += f"> {label} : **{cat.name}**\n" if cat else f"> {label} : N/A {catid}\n" else: - cats_text += f"> {label} : ⚠️ Non défini\n" - embed.add_field(name="📁 Catégories", value=cats_text, inline=False) + cats_text += f"> {label} : Non defini\n" + embed.add_field(name="Categories", value=cats_text, inline=False) await interaction.response.send_message(embed=embed, ephemeral=True) async def _creset(self, interaction): if not interaction.user.guild_permissions.administrator: - await interaction.response.send_message("❌ Réservé aux administrateurs.", ephemeral=True) + await interaction.response.send_message("Admin uniquement.", ephemeral=True) return save_guild_config(interaction.guild.id, {"roles": {}, "channels": {}, "categories": {}}) - await interaction.response.send_message("✅ Configuration réinitialisée.", ephemeral=True) + await interaction.response.send_message("Configuration reinitialisee.", ephemeral=True) # ═══════════════════════════════════════════════════════════════════ @@ -1195,20 +1176,18 @@ class DiversPanelView(discord.ui.LayoutView): self.bot_ref = bot items = [ - discord.ui.TextDisplay("## 🎲 Divers"), - discord.ui.TextDisplay("Lancer de dé et autres outils."), + discord.ui.TextDisplay("## Divers"), + discord.ui.TextDisplay("Lancer de de et autres outils."), discord.ui.Separator(), discord.ui.ActionRow( - _btn("🎲 Lancer un Dé", discord.ButtonStyle.primary, "dp_roll", self._roll), + _btn("Lancer un De", discord.ButtonStyle.primary, "dp_roll", self._roll), ), ] if author.guild_permissions.administrator: - items.append( - discord.ui.ActionRow( - _btn("📊 Stats Bot", discord.ButtonStyle.secondary, "dp_stats", self._stats), - ) - ) + items.append(discord.ui.ActionRow( + _btn("Stats Bot", discord.ButtonStyle.secondary, "dp_stats", self._stats), + )) items.extend([ discord.ui.Separator(), @@ -1218,9 +1197,7 @@ class DiversPanelView(discord.ui.LayoutView): self.add_item(discord.ui.Container(*items, accent_colour=discord.Colour(C_FUN))) async def _back(self, interaction): - await interaction.response.edit_message( - view=MainPanelView(interaction.user, self.bot_ref) - ) + await interaction.response.edit_message(view=MainPanelView(interaction.user, self.bot_ref)) async def _roll(self, interaction): await interaction.response.send_modal(RollModal()) @@ -1229,49 +1206,35 @@ class DiversPanelView(discord.ui.LayoutView): from utils.stats_manager import get_stats_detailed stats = get_stats_detailed() if not stats: - await interaction.response.send_message("📊 Aucune statistique.", ephemeral=True) + await interaction.response.send_message("Aucune statistique.", ephemeral=True) return await interaction.response.send_message( - f"📊 **Statistiques du bot**\n```json\n{json.dumps(stats, indent=2, ensure_ascii=False)[:1800]}\n```", + f"**Statistiques du bot**\n```json\n{json.dumps(stats, indent=2, ensure_ascii=False)[:1800]}\n```", ephemeral=True, ) # ═══════════════════════════════════════════════════════════════════ -# PANEL PRINCIPAL — MENU ADAPTATIF +# PANEL PRINCIPAL # ═══════════════════════════════════════════════════════════════════ class CategorySelect(discord.ui.Select): def __init__(self, author, bot): options = [ - discord.SelectOption(label="Mes Comptes", description="Comptes, virements, cartes", emoji="💰", value="compte"), + discord.SelectOption(label="Mes Comptes", description="Comptes, virements, releves", emoji="💰", value="compte"), ] if has_role(author, "banquier"): - options.append( - discord.SelectOption(label="Opérations Banquier", description="Ouvrir comptes, opérations", emoji="🏦", value="banquier") - ) + options.append(discord.SelectOption(label="Operations Banquier", description="Comptes, clients, reserve", emoji="🏦", value="banquier")) if has_role(author, "gouvernement"): - options.append( - discord.SelectOption(label="Gouvernement", description="Banques, institutions, fisc", emoji="🏛️", value="gouvernement") - ) - options.append( - discord.SelectOption(label="Identité", description="Cartes d'identité, permis", emoji="🪪", value="identite") - ) + options.append(discord.SelectOption(label="Gouvernement", description="Banques, institutions, fisc", emoji="🏛️", value="gouvernement")) + options.append(discord.SelectOption(label="Identite", description="Cartes, permis, medicale", emoji="🪪", value="identite")) if author.guild_permissions.administrator: - options.append( - discord.SelectOption(label="Tickets & Messages", description="Tickets, MP privés", emoji="📩", value="tickets") - ) - options.append( - discord.SelectOption(label="Sessions RP", description="Annonces, présences", emoji="📅", value="sessions") - ) - options.append( - discord.SelectOption(label="Divers", description="Dé, stats", emoji="🎲", value="divers") - ) + options.append(discord.SelectOption(label="Tickets & Messages", description="Tickets, MP prives", emoji="📩", value="tickets")) + options.append(discord.SelectOption(label="Sessions RP", description="Annonces, presences", emoji="📅", value="sessions")) + options.append(discord.SelectOption(label="Divers", description="De, stats", emoji="🎲", value="divers")) if author.guild_permissions.manage_guild: - options.append( - discord.SelectOption(label="Configuration", description="Configurer le bot", emoji="⚙️", value="config") - ) - super().__init__(placeholder="Sélectionnez une catégorie...", options=options, custom_id="pnl_catselect") + options.append(discord.SelectOption(label="Configuration", description="Configurer le bot", emoji="⚙️", value="config")) + super().__init__(placeholder="Selectionnez une categorie...", options=options, custom_id=_uid("pnl_cat_" + str(author.id))) self._bot = bot async def callback(self, interaction: discord.Interaction): @@ -1302,23 +1265,23 @@ class MainPanelView(discord.ui.LayoutView): role_parts = [] if has_role(author, "banquier"): - role_parts.append("🏦 Banquier") + role_parts.append("Banquier") if has_role(author, "gouvernement"): - role_parts.append("🏛️ Gouvernement") + role_parts.append("Gouvernement") if has_role(author, "ems"): - role_parts.append("🏥 EMS") + role_parts.append("EMS") if has_role(author, "auto_ecole"): - role_parts.append("🚗 Auto-école") + role_parts.append("Auto-ecole") if author.guild_permissions.administrator: - role_parts.append("👑 Admin") - role_text = " • ".join(role_parts) if role_parts else "👤 Citoyen" + role_parts.append("Admin") + role_text = " | ".join(role_parts) if role_parts else "Citoyen" guild_name = author.guild.name if author.guild else "Victoria's Vice" self.add_item(discord.ui.Container( - discord.ui.TextDisplay(f"## 🏦 {guild_name} — Panel de Commandes"), - discord.ui.TextDisplay(f"**Vos rôles :** {role_text}"), - discord.ui.TextDisplay("Sélectionnez une catégorie ci-dessous pour accéder aux commandes."), + discord.ui.TextDisplay(f"## {guild_name} — Panel de Commandes"), + discord.ui.TextDisplay(f"**Vos roles :** {role_text}"), + discord.ui.TextSelect("Selectionnez une categorie ci-dessous."), discord.ui.Separator(), discord.ui.ActionRow(CategorySelect(author, bot)), accent_colour=discord.Colour(C_MAIN), @@ -1327,10 +1290,7 @@ class MainPanelView(discord.ui.LayoutView): async def on_timeout(self): if self.message: try: - await self.message.edit( - content="⏰ Panel expiré. Utilisez `/panel` pour le rouvrir.", - view=None, - ) + await self.message.edit(content="Panel expire. Utilisez `/panel` pour le rouvrir.", view=None) except discord.HTTPException: pass diff --git a/utils/bank_card.py b/utils/bank_card.py index cf6d320..f2fd22d 100644 --- a/utils/bank_card.py +++ b/utils/bank_card.py @@ -1,25 +1,13 @@ import os import math +import hashlib from io import BytesIO -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw, ImageFont, ImageFilter class BankCardGenerator: WIDTH = 856 HEIGHT = 540 - RADIUS = 30 - - COLORS = { - "dark": (25, 25, 35), - "mid": (35, 40, 60), - "light": (50, 55, 80), - "accent": (0, 180, 160), - "accent_dim": (0, 120, 105), - "gold": (212, 175, 55), - "white": (255, 255, 255), - "gray": (160, 165, 180), - "dim": (90, 95, 110), - } def __init__(self): self.font_title = ImageFont.load_default() @@ -27,6 +15,8 @@ class BankCardGenerator: self.font_value = ImageFont.load_default() self.font_rib = ImageFont.load_default() self.font_large = ImageFont.load_default() + self.font_huge = ImageFont.load_default() + self.font_small = ImageFont.load_default() try: base = os.path.join(os.path.dirname(__file__), "..", "ressources", "fonts") if os.path.isdir(base): @@ -34,27 +24,30 @@ class BankCardGenerator: path = os.path.join(base, name) lower = name.lower() if "bold" in lower: - self.font_title = ImageFont.truetype(path, 26) - self.font_large = ImageFont.truetype(path, 36) - elif "mono" in lower or "regular" in lower: - self.font_label = ImageFont.truetype(path, 16) + self.font_title = ImageFont.truetype(path, 28) + self.font_large = ImageFont.truetype(path, 38) + self.font_huge = ImageFont.truetype(path, 48) + elif "mono" in lower or "regular" in lower or "light" in lower: + self.font_label = ImageFont.truetype(path, 15) self.font_value = ImageFont.truetype(path, 22) - self.font_rib = ImageFont.truetype(path, 20) + self.font_rib = ImageFont.truetype(path, 24) + self.font_small = ImageFont.truetype(path, 12) except Exception: pass - def _rounded_rect(self, draw, xy, radius, fill, outline=None, width=0): + def _rounded_rect(self, draw, xy, radius, fill=None, outline=None, width=0): x0, y0, x1, y1 = xy - r = radius - draw.ellipse([x0, y0, x0 + 2 * r, y0 + 2 * r], fill=fill, outline=outline, width=width) - draw.ellipse([x1 - 2 * r, y0, x1, y0 + 2 * r], fill=fill, outline=outline, width=width) - draw.ellipse([x0, y1 - 2 * r, x0 + 2 * r, y1], fill=fill, outline=outline, width=width) - draw.ellipse([x1 - 2 * r, y1 - 2 * r, x1, y1], fill=fill, outline=outline, width=width) - draw.rectangle([x0 + r, y0, x1 - r, y1], fill=fill) - draw.rectangle([x0, y0 + r, x1, y1 - r], fill=fill) + r = min(radius, (x1 - x0) // 2, (y1 - y0) // 2) + if fill: + draw.ellipse([x0, y0, x0 + 2 * r, y0 + 2 * r], fill=fill) + draw.ellipse([x1 - 2 * r, y0, x1, y0 + 2 * r], fill=fill) + draw.ellipse([x0, y1 - 2 * r, x0 + 2 * r, y1], fill=fill) + draw.ellipse([x1 - 2 * r, y1 - 2 * r, x1, y1], fill=fill) + draw.rectangle([x0 + r, y0, x1 - r, y1], fill=fill) + draw.rectangle([x0, y0 + r, x1, y1 - r], fill=fill) if outline and width: draw.arc([x0, y0, x0 + 2 * r, y0 + 2 * r], 180, 270, fill=outline, width=width) - draw.arc([x1 - 2 * r, y0, x1, y0 + 2 * r], 270, 0, fill=outline, width=width) + draw.arc([x1 - 2 * r, y0, x1, y0 + 2 * r], 270, 360, fill=outline, width=width) draw.arc([x0, y1 - 2 * r, x0 + 2 * r, y1], 90, 180, fill=outline, width=width) draw.arc([x1 - 2 * r, y1 - 2 * r, x1, y1], 0, 90, fill=outline, width=width) draw.line([x0 + r, y0, x1 - r, y0], fill=outline, width=width) @@ -62,40 +55,65 @@ class BankCardGenerator: draw.line([x0, y0 + r, x0, y1 - r], fill=outline, width=width) draw.line([x1, y0 + r, x1, y1 - r], fill=outline, width=width) - def _draw_gradient_rect(self, draw, xy, color_start, color_end, vertical=True): + def _gradient(self, draw, xy, c1, c2, vertical=True): x0, y0, x1, y1 = xy - if vertical: - for y in range(y0, y1): - t = (y - y0) / max(1, y1 - y0) - r = int(color_start[0] + (color_end[0] - color_start[0]) * t) - g = int(color_start[1] + (color_end[1] - color_start[1]) * t) - b = int(color_start[2] + (color_end[2] - color_start[2]) * t) - draw.line([(x0, y), (x1, y)], fill=(r, g, b)) - else: - for x in range(x0, x1): - t = (x - x0) / max(1, x1 - x0) - r = int(color_start[0] + (color_end[0] - color_start[0]) * t) - g = int(color_start[1] + (color_end[1] - color_start[1]) * t) - b = int(color_start[2] + (color_end[2] - color_start[2]) * t) - draw.line([(x, y0), (x, y1)], fill=(r, g, b)) + rng = range(y0, y1) if vertical else range(x0, x1) + length = (y1 - y0) if vertical else (x1 - x0) + for i in rng: + t = (i - (y0 if vertical else x0)) / max(1, length) + r = int(c1[0] + (c2[0] - c1[0]) * t) + g = int(c1[1] + (c2[1] - c1[1]) * t) + b = int(c1[2] + (c2[2] - c1[2]) * t) + if vertical: + draw.line([(x0, i), (x1, i)], fill=(r, g, b)) + else: + draw.line([(i, y0), (i, y1)], fill=(r, g, b)) def _draw_chip(self, draw, x, y): - w, h = 55, 40 - self._rounded_rect(draw, (x, y, x + w, y + h), 6, fill=(200, 175, 50), outline=(180, 155, 40), width=2) - draw.line([(x + 8, y + h // 3), (x + w - 8, y + h // 3)], fill=(160, 135, 30), width=1) - draw.line([(x + 8, y + 2 * h // 3), (x + w - 8, y + 2 * h // 3)], fill=(160, 135, 30), width=1) - draw.line([(x + w // 3, y + 4), (x + w // 3, y + h - 4)], fill=(160, 135, 30), width=1) - draw.line([(x + 2 * w // 3, y + 4), (x + 2 * w // 3, y + h - 4)], fill=(160, 135, 30), width=1) + w, h = 60, 45 + self._rounded_rect(draw, (x, y, x + w, y + h), 8, fill=(210, 180, 55)) + self._rounded_rect(draw, (x + 3, y + 3, x + w - 3, y + h - 3), 6, fill=(225, 195, 70)) + for dy in [h // 3, 2 * h // 3]: + draw.line([(x + 8, y + dy), (x + w - 8, y + dy)], fill=(190, 160, 40), width=1) + for dx in [w // 3, 2 * w // 3]: + draw.line([(x + dx, y + 5), (x + dx, y + h - 5)], fill=(190, 160, 40), width=1) + draw.arc([x + w // 2 - 12, y + h // 2 - 12, x + w // 2 + 12, y + h // 2 + 12], 0, 360, fill=(190, 160, 40), width=1) - def _draw_wave(self, draw, y_base, color, amplitude=15, segments=8): - points = [] - w = self.WIDTH - for i in range(segments + 1): - x = int(w * i / segments) - y = y_base + int(amplitude * math.sin(i * 1.2)) - points.append((x, y)) - for i in range(len(points) - 1): - draw.line([points[i], points[i + 1]], fill=color, width=2) + def _draw_hologram(self, draw, x, y, size=50): + for i in range(5): + offset = i * 3 + alpha_color = ( + 100 + i * 30, + 200 - i * 20, + 220 - i * 15, + ) + draw.ellipse( + [x + offset, y + offset, x + size - offset, y + size - offset], + outline=alpha_color, width=1, + ) + draw.ellipse([x + size // 2 - 5, y + size // 2 - 5, x + size // 2 + 5, y + size // 2 + 5], fill=(180, 220, 240)) + + def _draw_contactless(self, draw, x, y): + for i, r in enumerate([10, 16, 22]): + draw.arc([x - r, y - r, x + r, y + r], -60, 60, fill=(180, 180, 190), width=1) + + def _draw_security_pattern(self, draw, width, height): + for x in range(0, width, 40): + for y in range(0, height, 40): + h = hashlib.md5(f"{x}{y}".encode()).hexdigest() + if int(h[:2], 16) < 15: + draw.point((x, y), fill=(60, 65, 85)) + + def _draw_glow(self, img, x, y, radius=80, color=(0, 180, 160), intensity=30): + overlay = Image.new("RGBA", img.size, (0, 0, 0, 0)) + od = ImageDraw.Draw(overlay) + for r in range(radius, 0, -2): + alpha = int(intensity * (r / radius)) + od.ellipse( + [x - r, y - r, x + r, y + r], + fill=(*color, alpha), + ) + return Image.alpha_composite(img.convert("RGBA"), overlay) def generate( self, @@ -106,51 +124,84 @@ class BankCardGenerator: account_type: str = "particulier", tva_rate: float = 0.0, ) -> BytesIO: - img = Image.new("RGB", (self.WIDTH, self.HEIGHT), self.COLORS["dark"]) + dark1 = (18, 18, 28) + dark2 = (30, 32, 48) + accent = (0, 190, 170) + accent_dim = (0, 130, 115) + gold = (212, 175, 55) + white = (245, 245, 250) + gray = (150, 155, 170) + dim = (80, 85, 100) + + img = Image.new("RGB", (self.WIDTH, self.HEIGHT), dark1) draw = ImageDraw.Draw(img) - self._draw_gradient_rect(draw, (0, 0, self.WIDTH, self.HEIGHT), self.COLORS["dark"], self.COLORS["mid"]) + self._gradient(draw, (0, 0, self.WIDTH, self.HEIGHT), dark1, dark2) - self._draw_wave(draw, 120, (*self.COLORS["accent"][:3],), amplitude=20, segments=10) - self._draw_wave(draw, 140, (*self.COLORS["accent_dim"][:3],), amplitude=15, segments=12) - self._draw_wave(draw, 360, (*self.COLORS["light"][:3],), amplitude=12, segments=8) + self._draw_security_pattern(draw, self.WIDTH, self.HEIGHT) - self._rounded_rect(draw, (20, 20, self.WIDTH - 20, self.HEIGHT - 20), self.RADIUS, fill=None, outline=self.COLORS["accent"], width=2) + for wave_y, color, amp, seg in [ + (100, (*accent[:3],), 25, 10), + (130, (*accent_dim[:3],), 18, 12), + (350, (55, 60, 85), 12, 8), + ]: + pts = [] + for i in range(seg + 1): + px = int(self.WIDTH * i / seg) + py = wave_y + int(amp * math.sin(i * 1.3)) + pts.append((px, py)) + for i in range(len(pts) - 1): + draw.line([pts[i], pts[i + 1]], fill=color, width=2) - self._draw_chip(draw, 60, 130) + self._rounded_rect(draw, (18, 18, self.WIDTH - 18, self.HEIGHT - 18), 28, outline=accent, width=2) - draw.text((75, 185), "VICTORIA'S VICE", fill=self.COLORS["accent"], font=self.font_title) - draw.text((75, 218), bank_name.upper(), fill=self.COLORS["gray"], font=self.font_label) + img = self._draw_glow(img, 85, 155, radius=70, color=accent, intensity=20) + draw = ImageDraw.Draw(img) + + self._draw_chip(draw, 55, 125) + self._draw_hologram(draw, self.WIDTH - 120, 120, size=55) + self._draw_contactless(draw, self.WIDTH - 80, 80) + + draw.text((65, 182), "VICTORIA'S VICE", fill=accent, font=self.font_title) + draw.text((65, 215), bank_name.upper(), fill=gray, font=self.font_label) type_label = "COMPTE ENTREPRISE" if account_type == "entreprise" else "COMPTE PARTICULIER" - draw.text((self.WIDTH - 200, 45), type_label, fill=self.COLORS["dim"], font=self.font_label) + draw.text((self.WIDTH - 210, 42), type_label, fill=dim, font=self.font_label) - y_rib = 280 - draw.text((60, y_rib), "RIB", fill=self.COLORS["dim"], font=self.font_label) - - rib_parts = [] clean_rib = rib.replace(" ", "") - for i in range(0, len(clean_rib), 4): - rib_parts.append(clean_rib[i:i + 4]) - rib_display = " ".join(rib_parts) - draw.text((60, y_rib + 22), rib_display, fill=self.COLORS["white"], font=self.font_rib) + rib_display = " ".join(clean_rib[i:i + 4] for i in range(0, len(clean_rib), 4)) - y_solde = 350 - draw.text((60, y_solde), "SOLDE", fill=self.COLORS["dim"], font=self.font_label) + y_card_number = 260 + draw.text((60, y_card_number - 18), "RIB", fill=dim, font=self.font_label) + draw.text((60, y_card_number), rib_display, fill=white, font=self.font_rib) + + y_solde = 340 + draw.text((60, y_solde - 18), "SOLDE DISPONIBLE", fill=dim, font=self.font_label) solde_str = f"{solde:,.2f} EUR".replace(",", " ") - draw.text((60, y_solde + 22), solde_str, fill=self.COLORS["gold"], font=self.font_large) - - y_bottom = self.HEIGHT - 100 - draw.text((60, y_bottom), "TITULAIRE", fill=self.COLORS["dim"], font=self.font_label) - draw.text((60, y_bottom + 20), holder_name.upper(), fill=self.COLORS["white"], font=self.font_value) + draw.text((60, y_solde), solde_str, fill=gold, font=self.font_large) if account_type == "entreprise" and tva_rate > 0: - draw.text((self.WIDTH - 200, y_bottom), "TVA", fill=self.COLORS["dim"], font=self.font_label) - draw.text((self.WIDTH - 200, y_bottom + 20), f"{tva_rate}%", fill=self.COLORS["accent"], font=self.font_value) + draw.text((self.WIDTH - 180, y_solde - 18), "TAUX TVA", fill=dim, font=self.font_label) + draw.text((self.WIDTH - 180, y_solde), f"{tva_rate}%", fill=accent, font=self.font_value) - for i in range(5): - x = self.WIDTH - 60 - i * 35 - draw.ellipse([x, self.HEIGHT - 50, x + 25, self.HEIGHT - 25], fill=self.COLORS["accent"] if i < 3 else self.COLORS["dim"]) + y_bottom = self.HEIGHT - 110 + draw.line([(60, y_bottom - 10), (self.WIDTH - 60, y_bottom - 10)], fill=(60, 65, 85), width=1) + + draw.text((60, y_bottom), "TITULAIRE", fill=dim, font=self.font_label) + draw.text((60, y_bottom + 18), holder_name.upper(), fill=white, font=self.font_value) + + sig_y = y_bottom + 5 + for i in range(40): + sx = self.WIDTH - 250 + i * 3 + sy = sig_y + int(3 * math.sin(i * 0.5)) + draw.point((sx, sy), fill=(120, 125, 140)) + draw.point((sx, sy + 1), fill=(120, 125, 140)) + + for i in range(4): + cx = self.WIDTH - 65 - i * 30 + draw.ellipse([cx, self.HEIGHT - 45, cx + 28, self.HEIGHT - 17], fill=accent if i < 2 else dim) + + draw.text((55, self.HEIGHT - 35), "VIRTUAL CARD", fill=dim, font=self.font_small) buf = BytesIO() img.save(buf, format="PNG") diff --git a/utils/bank_statement.py b/utils/bank_statement.py new file mode 100644 index 0000000..21c5575 --- /dev/null +++ b/utils/bank_statement.py @@ -0,0 +1,188 @@ +import os +from io import BytesIO +from PIL import Image, ImageDraw, ImageFont + + +class BankStatementGenerator: + WIDTH = 900 + ROW_HEIGHT = 38 + HEADER_HEIGHT = 180 + FOOTER_HEIGHT = 80 + + def __init__(self): + self.font_title = ImageFont.load_default() + self.font_header = ImageFont.load_default() + self.font_body = ImageFont.load_default() + self.font_body_bold = ImageFont.load_default() + self.font_small = ImageFont.load_default() + self.font_amount_pos = ImageFont.load_default() + self.font_amount_neg = ImageFont.load_default() + try: + base = os.path.join(os.path.dirname(__file__), "..", "ressources", "fonts") + if os.path.isdir(base): + for name in os.listdir(base): + path = os.path.join(base, name) + lower = name.lower() + if "bold" in lower: + self.font_title = ImageFont.truetype(path, 30) + self.font_header = ImageFont.truetype(path, 16) + self.font_body_bold = ImageFont.truetype(path, 15) + self.font_amount_pos = ImageFont.truetype(path, 15) + self.font_amount_neg = ImageFont.truetype(path, 15) + elif "mono" in lower or "regular" in lower or "light" in lower: + self.font_body = ImageFont.truetype(path, 14) + self.font_small = ImageFont.truetype(path, 12) + except Exception: + pass + + def _rounded_rect(self, draw, xy, radius, fill=None, outline=None, width=0): + x0, y0, x1, y1 = xy + r = min(radius, (x1 - x0) // 2, (y1 - y0) // 2) + if fill: + draw.ellipse([x0, y0, x0 + 2 * r, y0 + 2 * r], fill=fill) + draw.ellipse([x1 - 2 * r, y0, x1, y0 + 2 * r], fill=fill) + draw.ellipse([x0, y1 - 2 * r, x0 + 2 * r, y1], fill=fill) + draw.ellipse([x1 - 2 * r, y1 - 2 * r, x1, y1], fill=fill) + draw.rectangle([x0 + r, y0, x1 - r, y1], fill=fill) + draw.rectangle([x0, y0 + r, x1, y1 - r], fill=fill) + if outline and width: + draw.line([x0 + r, y0, x1 - r, y0], fill=outline, width=width) + draw.line([x0 + r, y1, x1 - r, y1], fill=outline, width=width) + draw.line([x0, y0 + r, x0, y1 - r], fill=outline, width=width) + draw.line([x1, y0 + r, x1, y1 - r], fill=outline, width=width) + + def generate( + self, + bank_name: str, + holder_name: str, + rib: str, + solde_actuel: float, + transactions: list, + account_type: str = "particulier", + ) -> BytesIO: + bg = (15, 15, 22) + card_bg = (22, 24, 35) + accent = (0, 190, 170) + gold = (212, 175, 55) + white = (240, 240, 245) + gray = (140, 145, 160) + dim = (70, 75, 90) + row_even = (26, 28, 40) + row_odd = (22, 24, 35) + green = (40, 200, 120) + red = (220, 70, 70) + line_color = (40, 42, 55) + + n_rows = len(transactions) + total_h = self.HEADER_HEIGHT + n_rows * self.ROW_HEIGHT + self.FOOTER_HEIGHT + 60 + + img = Image.new("RGB", (self.WIDTH, total_h), bg) + draw = ImageDraw.Draw(img) + + self._rounded_rect(draw, (20, 20, self.WIDTH - 20, 50), 10, fill=card_bg) + draw.text((35, 25), "RELEVE BANCAIRE", fill=accent, font=self.font_title) + + y = 80 + draw.line([(30, y), (self.WIDTH - 30, y)], fill=line_color, width=1) + y += 15 + + col_labels = [ + (35, "DATE"), + (160, "TYPE"), + (380, "EMETTEUR / DESTINATAIRE"), + (620, "MONTANT"), + ] + for cx, label in col_labels: + draw.text((cx, y), label, fill=gray, font=self.font_header) + y += 25 + + draw.text((35, y), f"Banque : {bank_name}", fill=white, font=self.font_body_bold) + draw.text((35, y + 20), f"Titulaire : {holder_name}", fill=white, font=self.font_body) + draw.text((35, y + 40), f"RIB : {rib}", fill=gray, font=self.font_small) + + solde_label = f"Solde actuel : {solde_actuel:,.2f} EUR".replace(",", " ") + draw.text((self.WIDTH - 300, y + 10), solde_label, fill=gold, font=self.font_body_bold) + type_label = "Entreprise" if account_type == "entreprise" else "Particulier" + draw.text((self.WIDTH - 300, y + 32), f"Type : {type_label}", fill=gray, font=self.font_small) + + y = self.HEADER_HEIGHT + 20 + draw.line([(30, y - 5), (self.WIDTH - 30, y - 5)], fill=line_color, width=1) + + for i, tx in enumerate(transactions): + ry = y + i * self.ROW_HEIGHT + row_bg = row_even if i % 2 == 0 else row_odd + draw.rectangle([(30, ry), (self.WIDTH - 30, ry + self.ROW_HEIGHT)], fill=row_bg) + + date_str = tx.get("date", "N/A") + if len(date_str) > 16: + date_str = date_str[:16] + + tx_type = tx.get("type", "N/A") + type_display = { + "virement": "Virement", + "depot_banquier": "Depot", + "retrait_banquier": "Retrait", + "tva_prelevee": "TVA Prelevee", + }.get(tx_type, tx_type) + + emetteur_rib = tx.get("emetteur_rib", "") + destinataire_rib = tx.get("destinataire_rib", "") + montant_val = tx.get("montant", 0) + + emetteur_id = tx.get("emetteur_id") + destinataire_id = tx.get("destinataire_id") + if emetteur_id: + other_ref = f"De: <@{emetteur_id}>" + elif emetteur_rib: + other_ref = f"De: ...{emetteur_rib[-6:]}" if emetteur_rib else "" + elif destinataire_id: + other_ref = f"A: <@{destinataire_id}>" + elif destinataire_rib: + other_ref = f"A: ...{destinataire_rib[-6:]}" if destinataire_rib else "" + else: + other_ref = "" + + metadata = tx.get("metadata", "") + if metadata: + try: + import json + meta = json.loads(metadata) + if "banquier_id" in meta: + other_ref += f" (banquier: {meta['banquier_id']})" + except Exception: + pass + + draw.text((35, ry + 10), date_str, fill=gray, font=self.font_body) + draw.text((160, ry + 10), type_display, fill=white, font=self.font_body_bold) + draw.text((380, ry + 10), other_ref, fill=gray, font=self.font_body) + + is_incoming = tx_type in ("depot_banquier",) or ( + tx_type == "virement" and destinataire_rib and not emetteur_rib + ) + if tx_type == "virement": + is_incoming = False + if tx_type == "tva_prelevee": + amt_color = red + amt_str = f"-{montant_val:,.2f} EUR".replace(",", " ") + elif is_incoming: + amt_color = green + amt_str = f"+{montant_val:,.2f} EUR".replace(",", " ") + else: + amt_color = red if montant_val > 0 else gray + amt_str = f"-{montant_val:,.2f} EUR".replace(",", " ") + + draw.text((620, ry + 10), amt_str, fill=amt_color, font=self.font_amount_pos) + + draw.line([(30, ry + self.ROW_HEIGHT), (self.WIDTH - 30, ry + self.ROW_HEIGHT)], fill=line_color, width=1) + + footer_y = y + n_rows * self.ROW_HEIGHT + 15 + draw.line([(30, footer_y), (self.WIDTH - 30, footer_y)], fill=line_color, width=1) + footer_y += 10 + + draw.text((35, footer_y), f"{n_rows} transaction(s)", fill=gray, font=self.font_small) + draw.text((35, footer_y + 18), "Victoria's Vice — Systeme Bancaire V2", fill=dim, font=self.font_small) + + buf = BytesIO() + img.save(buf, format="PNG") + buf.seek(0) + return buf