nanopy-bank 1.0.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. nanopy_bank/__init__.py +20 -0
  2. nanopy_bank/api/__init__.py +10 -0
  3. nanopy_bank/api/server.py +242 -0
  4. nanopy_bank/app.py +282 -0
  5. nanopy_bank/cli.py +152 -0
  6. nanopy_bank/core/__init__.py +85 -0
  7. nanopy_bank/core/audit.py +404 -0
  8. nanopy_bank/core/auth.py +306 -0
  9. nanopy_bank/core/bank.py +407 -0
  10. nanopy_bank/core/beneficiary.py +258 -0
  11. nanopy_bank/core/branch.py +319 -0
  12. nanopy_bank/core/fees.py +243 -0
  13. nanopy_bank/core/holding.py +416 -0
  14. nanopy_bank/core/models.py +308 -0
  15. nanopy_bank/core/products.py +300 -0
  16. nanopy_bank/data/__init__.py +31 -0
  17. nanopy_bank/data/demo.py +846 -0
  18. nanopy_bank/documents/__init__.py +11 -0
  19. nanopy_bank/documents/statement.py +304 -0
  20. nanopy_bank/sepa/__init__.py +10 -0
  21. nanopy_bank/sepa/sepa.py +452 -0
  22. nanopy_bank/storage/__init__.py +11 -0
  23. nanopy_bank/storage/json_storage.py +127 -0
  24. nanopy_bank/storage/sqlite_storage.py +326 -0
  25. nanopy_bank/ui/__init__.py +14 -0
  26. nanopy_bank/ui/pages/__init__.py +33 -0
  27. nanopy_bank/ui/pages/accounts.py +85 -0
  28. nanopy_bank/ui/pages/advisor.py +140 -0
  29. nanopy_bank/ui/pages/audit.py +73 -0
  30. nanopy_bank/ui/pages/beneficiaries.py +115 -0
  31. nanopy_bank/ui/pages/branches.py +64 -0
  32. nanopy_bank/ui/pages/cards.py +36 -0
  33. nanopy_bank/ui/pages/common.py +18 -0
  34. nanopy_bank/ui/pages/dashboard.py +100 -0
  35. nanopy_bank/ui/pages/fees.py +60 -0
  36. nanopy_bank/ui/pages/holding.py +943 -0
  37. nanopy_bank/ui/pages/loans.py +105 -0
  38. nanopy_bank/ui/pages/login.py +174 -0
  39. nanopy_bank/ui/pages/sepa.py +118 -0
  40. nanopy_bank/ui/pages/settings.py +48 -0
  41. nanopy_bank/ui/pages/transfers.py +94 -0
  42. nanopy_bank/ui/pages.py +16 -0
  43. nanopy_bank-1.0.8.dist-info/METADATA +72 -0
  44. nanopy_bank-1.0.8.dist-info/RECORD +47 -0
  45. nanopy_bank-1.0.8.dist-info/WHEEL +5 -0
  46. nanopy_bank-1.0.8.dist-info/entry_points.txt +2 -0
  47. nanopy_bank-1.0.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,20 @@
1
+ """
2
+ NanoPy Bank - Online Banking System
3
+ """
4
+
5
+ __version__ = "1.0.0"
6
+ __author__ = "NanoPy Team"
7
+
8
+ from .core import Account, Transaction, Customer, TransactionType, Bank, get_bank
9
+ from .sepa import SEPAGenerator, SEPAParser
10
+
11
+ __all__ = [
12
+ "Account",
13
+ "Transaction",
14
+ "Customer",
15
+ "TransactionType",
16
+ "Bank",
17
+ "get_bank",
18
+ "SEPAGenerator",
19
+ "SEPAParser",
20
+ ]
@@ -0,0 +1,10 @@
1
+ """
2
+ API Module - REST API for banking operations
3
+ """
4
+
5
+ from .server import BankAPI, run_api
6
+
7
+ __all__ = [
8
+ "BankAPI",
9
+ "run_api",
10
+ ]
@@ -0,0 +1,242 @@
1
+ """
2
+ REST API Server for NanoPy Bank
3
+ """
4
+
5
+ from aiohttp import web
6
+ from decimal import Decimal
7
+ from typing import Optional
8
+ import json
9
+
10
+ from ..core import Bank, get_bank, TransactionType, AccountType, Currency
11
+
12
+
13
+ class BankAPI:
14
+ """
15
+ REST API for banking operations
16
+ """
17
+
18
+ def __init__(self, bank: Optional[Bank] = None, host: str = "0.0.0.0", port: int = 8888):
19
+ self.bank = bank or get_bank()
20
+ self.host = host
21
+ self.port = port
22
+ self.app = web.Application()
23
+ self._setup_routes()
24
+
25
+ def _setup_routes(self):
26
+ """Setup API routes"""
27
+ # Health
28
+ self.app.router.add_get("/api/health", self.api_health)
29
+ self.app.router.add_get("/api/stats", self.api_stats)
30
+
31
+ # Customers
32
+ self.app.router.add_get("/api/customers", self.api_customers)
33
+ self.app.router.add_post("/api/customers", self.api_create_customer)
34
+ self.app.router.add_get("/api/customers/{customer_id}", self.api_get_customer)
35
+
36
+ # Accounts
37
+ self.app.router.add_get("/api/accounts", self.api_accounts)
38
+ self.app.router.add_post("/api/accounts", self.api_create_account)
39
+ self.app.router.add_get("/api/accounts/{iban}", self.api_get_account)
40
+ self.app.router.add_get("/api/accounts/{iban}/balance", self.api_get_balance)
41
+ self.app.router.add_get("/api/accounts/{iban}/transactions", self.api_get_transactions)
42
+
43
+ # Transactions
44
+ self.app.router.add_post("/api/credit", self.api_credit)
45
+ self.app.router.add_post("/api/debit", self.api_debit)
46
+ self.app.router.add_post("/api/transfer", self.api_transfer)
47
+ self.app.router.add_post("/api/sepa/credit-transfer", self.api_sepa_credit_transfer)
48
+
49
+ # ========== HEALTH ==========
50
+
51
+ async def api_health(self, request):
52
+ """Health check"""
53
+ return web.json_response({"status": "ok", "service": "nanopy-bank"})
54
+
55
+ async def api_stats(self, request):
56
+ """Get bank statistics"""
57
+ stats = self.bank.get_stats()
58
+ return web.json_response(stats)
59
+
60
+ # ========== CUSTOMERS ==========
61
+
62
+ async def api_customers(self, request):
63
+ """List all customers"""
64
+ customers = [c.to_dict() for c in self.bank.customers.values()]
65
+ return web.json_response({"customers": customers, "count": len(customers)})
66
+
67
+ async def api_create_customer(self, request):
68
+ """Create a new customer"""
69
+ try:
70
+ data = await request.json()
71
+ customer = self.bank.create_customer(
72
+ first_name=data["first_name"],
73
+ last_name=data["last_name"],
74
+ email=data["email"],
75
+ phone=data.get("phone", ""),
76
+ address=data.get("address", ""),
77
+ city=data.get("city", ""),
78
+ postal_code=data.get("postal_code", ""),
79
+ country=data.get("country", "FR")
80
+ )
81
+ return web.json_response({"ok": True, "customer": customer.to_dict()})
82
+ except KeyError as e:
83
+ return web.json_response({"error": f"Missing field: {e}"}, status=400)
84
+ except Exception as e:
85
+ return web.json_response({"error": str(e)}, status=500)
86
+
87
+ async def api_get_customer(self, request):
88
+ """Get customer by ID"""
89
+ customer_id = request.match_info["customer_id"]
90
+ customer = self.bank.get_customer(customer_id)
91
+ if not customer:
92
+ return web.json_response({"error": "Customer not found"}, status=404)
93
+ return web.json_response(customer.to_dict())
94
+
95
+ # ========== ACCOUNTS ==========
96
+
97
+ async def api_accounts(self, request):
98
+ """List all accounts"""
99
+ accounts = [a.to_dict() for a in self.bank.accounts.values()]
100
+ return web.json_response({"accounts": accounts, "count": len(accounts)})
101
+
102
+ async def api_create_account(self, request):
103
+ """Create a new account"""
104
+ try:
105
+ data = await request.json()
106
+ account = self.bank.create_account(
107
+ customer_id=data["customer_id"],
108
+ account_type=AccountType(data.get("account_type", "checking")),
109
+ currency=Currency(data.get("currency", "EUR")),
110
+ initial_balance=Decimal(str(data.get("initial_balance", "0.00"))),
111
+ overdraft_limit=Decimal(str(data.get("overdraft_limit", "0.00"))),
112
+ account_name=data.get("account_name", "")
113
+ )
114
+ return web.json_response({"ok": True, "account": account.to_dict()})
115
+ except KeyError as e:
116
+ return web.json_response({"error": f"Missing field: {e}"}, status=400)
117
+ except Exception as e:
118
+ return web.json_response({"error": str(e)}, status=500)
119
+
120
+ async def api_get_account(self, request):
121
+ """Get account by IBAN"""
122
+ iban = request.match_info["iban"]
123
+ account = self.bank.get_account(iban)
124
+ if not account:
125
+ return web.json_response({"error": "Account not found"}, status=404)
126
+ return web.json_response(account.to_dict())
127
+
128
+ async def api_get_balance(self, request):
129
+ """Get account balance"""
130
+ iban = request.match_info["iban"]
131
+ account = self.bank.get_account(iban)
132
+ if not account:
133
+ return web.json_response({"error": "Account not found"}, status=404)
134
+ return web.json_response({
135
+ "iban": account.iban,
136
+ "balance": str(account.balance),
137
+ "available_balance": str(account.available_balance),
138
+ "currency": account.currency.value
139
+ })
140
+
141
+ async def api_get_transactions(self, request):
142
+ """Get account transactions"""
143
+ iban = request.match_info["iban"]
144
+ limit = int(request.query.get("limit", 50))
145
+ transactions = self.bank.get_account_transactions(iban, limit)
146
+ return web.json_response({
147
+ "iban": iban,
148
+ "transactions": [tx.to_dict() for tx in transactions],
149
+ "count": len(transactions)
150
+ })
151
+
152
+ # ========== TRANSACTIONS ==========
153
+
154
+ async def api_credit(self, request):
155
+ """Credit an account"""
156
+ try:
157
+ data = await request.json()
158
+ tx = self.bank.credit(
159
+ iban=data["iban"],
160
+ amount=Decimal(str(data["amount"])),
161
+ label=data["label"],
162
+ counterparty_name=data.get("counterparty_name", ""),
163
+ counterparty_iban=data.get("counterparty_iban", ""),
164
+ description=data.get("description", ""),
165
+ category=data.get("category", "")
166
+ )
167
+ return web.json_response({"ok": True, "transaction": tx.to_dict()})
168
+ except KeyError as e:
169
+ return web.json_response({"error": f"Missing field: {e}"}, status=400)
170
+ except Exception as e:
171
+ return web.json_response({"error": str(e)}, status=400)
172
+
173
+ async def api_debit(self, request):
174
+ """Debit an account"""
175
+ try:
176
+ data = await request.json()
177
+ tx = self.bank.debit(
178
+ iban=data["iban"],
179
+ amount=Decimal(str(data["amount"])),
180
+ label=data["label"],
181
+ counterparty_name=data.get("counterparty_name", ""),
182
+ counterparty_iban=data.get("counterparty_iban", ""),
183
+ description=data.get("description", ""),
184
+ category=data.get("category", "")
185
+ )
186
+ return web.json_response({"ok": True, "transaction": tx.to_dict()})
187
+ except KeyError as e:
188
+ return web.json_response({"error": f"Missing field: {e}"}, status=400)
189
+ except Exception as e:
190
+ return web.json_response({"error": str(e)}, status=400)
191
+
192
+ async def api_transfer(self, request):
193
+ """Transfer between accounts"""
194
+ try:
195
+ data = await request.json()
196
+ debit_tx, credit_tx = self.bank.transfer(
197
+ from_iban=data["from_iban"],
198
+ to_iban=data["to_iban"],
199
+ amount=Decimal(str(data["amount"])),
200
+ label=data["label"],
201
+ description=data.get("description", "")
202
+ )
203
+ return web.json_response({
204
+ "ok": True,
205
+ "debit_transaction": debit_tx.to_dict(),
206
+ "credit_transaction": credit_tx.to_dict() if credit_tx else None
207
+ })
208
+ except KeyError as e:
209
+ return web.json_response({"error": f"Missing field: {e}"}, status=400)
210
+ except Exception as e:
211
+ return web.json_response({"error": str(e)}, status=400)
212
+
213
+ async def api_sepa_credit_transfer(self, request):
214
+ """Execute SEPA Credit Transfer"""
215
+ try:
216
+ data = await request.json()
217
+ tx = self.bank.sepa_credit_transfer(
218
+ from_iban=data["from_iban"],
219
+ to_iban=data["to_iban"],
220
+ to_bic=data.get("to_bic", ""),
221
+ to_name=data["to_name"],
222
+ amount=Decimal(str(data["amount"])),
223
+ label=data["label"],
224
+ end_to_end_id=data.get("end_to_end_id", "")
225
+ )
226
+ return web.json_response({"ok": True, "transaction": tx.to_dict()})
227
+ except KeyError as e:
228
+ return web.json_response({"error": f"Missing field: {e}"}, status=400)
229
+ except Exception as e:
230
+ return web.json_response({"error": str(e)}, status=400)
231
+
232
+ def run(self):
233
+ """Run the API server"""
234
+ print(f"Starting NanoPy Bank API on http://{self.host}:{self.port}")
235
+ web.run_app(self.app, host=self.host, port=self.port, print=None)
236
+
237
+
238
+ def run_api(host: str = "0.0.0.0", port: int = 8888, data_dir: str = None):
239
+ """Run the API server"""
240
+ bank = get_bank(data_dir)
241
+ api = BankAPI(bank, host, port)
242
+ api.run()
nanopy_bank/app.py ADDED
@@ -0,0 +1,282 @@
1
+ """
2
+ NanoPy Bank - Streamlit UI with shadcn components
3
+ """
4
+
5
+ import streamlit as st
6
+ from streamlit_option_menu import option_menu
7
+
8
+ try:
9
+ from .core import get_bank
10
+ from .core.auth import get_auth_service, UserRole
11
+ from .ui.pages import (
12
+ render_dashboard, render_accounts, render_transfers,
13
+ render_beneficiaries, render_cards, render_loans,
14
+ render_fees, render_branches, render_sepa,
15
+ render_audit, render_settings, render_advisor, render_holding
16
+ )
17
+ from .ui.pages.login import render_login
18
+ except ImportError:
19
+ from nanopy_bank.core import get_bank
20
+ from nanopy_bank.core.auth import get_auth_service, UserRole
21
+ from nanopy_bank.ui.pages import (
22
+ render_dashboard, render_accounts, render_transfers,
23
+ render_beneficiaries, render_cards, render_loans,
24
+ render_fees, render_branches, render_sepa,
25
+ render_audit, render_settings, render_advisor, render_holding
26
+ )
27
+ from nanopy_bank.ui.pages.login import render_login
28
+
29
+
30
+ # Page config
31
+ st.set_page_config(
32
+ page_title="NanoPy Bank",
33
+ page_icon="🏦",
34
+ layout="wide",
35
+ initial_sidebar_state="expanded"
36
+ )
37
+
38
+ # Custom CSS
39
+ st.markdown("""
40
+ <style>
41
+ .stApp {
42
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
43
+ }
44
+ .main-header {
45
+ background: linear-gradient(90deg, #00d4ff, #7b2cbf);
46
+ -webkit-background-clip: text;
47
+ -webkit-text-fill-color: transparent;
48
+ font-size: 2.5rem;
49
+ font-weight: bold;
50
+ margin-bottom: 2rem;
51
+ }
52
+ .user-badge {
53
+ background: linear-gradient(135deg, #1e3a5f, #2d1b4e);
54
+ padding: 10px 15px;
55
+ border-radius: 10px;
56
+ margin-bottom: 15px;
57
+ }
58
+ </style>
59
+ """, unsafe_allow_html=True)
60
+
61
+
62
+ def init_session_state():
63
+ """Initialize session state"""
64
+ if "bank" not in st.session_state:
65
+ st.session_state.bank = get_bank()
66
+ if "current_account" not in st.session_state:
67
+ st.session_state.current_account = None
68
+ if "current_customer" not in st.session_state:
69
+ st.session_state.current_customer = None
70
+ if "logged_in" not in st.session_state:
71
+ st.session_state.logged_in = False
72
+ if "user" not in st.session_state:
73
+ st.session_state.user = None
74
+ if "session_id" not in st.session_state:
75
+ st.session_state.session_id = None
76
+
77
+
78
+ def logout():
79
+ """Logout user"""
80
+ if st.session_state.session_id:
81
+ auth = get_auth_service()
82
+ auth.logout(st.session_state.session_id)
83
+ st.session_state.logged_in = False
84
+ st.session_state.user = None
85
+ st.session_state.session_id = None
86
+ st.rerun()
87
+
88
+
89
+ def get_role_label(role: UserRole) -> str:
90
+ """Get French label for role"""
91
+ labels = {
92
+ UserRole.CLIENT: "Client",
93
+ UserRole.ADVISOR: "Conseiller",
94
+ UserRole.DIRECTOR: "Directeur",
95
+ UserRole.ADMIN: "Administrateur",
96
+ UserRole.HOLDING: "Holding",
97
+ }
98
+ return labels.get(role, "Utilisateur")
99
+
100
+
101
+ def get_role_color(role: UserRole) -> str:
102
+ """Get color for role"""
103
+ colors = {
104
+ UserRole.CLIENT: "#00d4ff",
105
+ UserRole.ADVISOR: "#7b2cbf",
106
+ UserRole.DIRECTOR: "#ff6b6b",
107
+ UserRole.ADMIN: "#ffd93d",
108
+ UserRole.HOLDING: "#00ff88",
109
+ }
110
+ return colors.get(role, "#888888")
111
+
112
+
113
+ def render_sidebar():
114
+ """Render sidebar navigation based on user role"""
115
+ user = st.session_state.user
116
+ role = user.role if user else UserRole.CLIENT
117
+
118
+ with st.sidebar:
119
+ # Logo
120
+ st.markdown("""
121
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
122
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#00d4ff" stroke-width="2">
123
+ <rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect>
124
+ <line x1="1" y1="10" x2="23" y2="10"></line>
125
+ </svg>
126
+ <span style="font-size: 1.5rem; font-weight: bold; background: linear-gradient(90deg, #00d4ff, #7b2cbf); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">NanoPy Bank</span>
127
+ </div>
128
+ """, unsafe_allow_html=True)
129
+
130
+ # User info
131
+ role_color = get_role_color(role)
132
+ st.markdown(f"""
133
+ <div class="user-badge">
134
+ <div style="font-size: 14px; font-weight: bold; color: white;">{user.display_name}</div>
135
+ <div style="font-size: 12px; color: {role_color};">{get_role_label(role)}</div>
136
+ </div>
137
+ """, unsafe_allow_html=True)
138
+
139
+ # Logout button
140
+ if st.button("Deconnexion", use_container_width=True):
141
+ logout()
142
+
143
+ st.divider()
144
+
145
+ page = None
146
+ page2 = None
147
+ page3 = None
148
+
149
+ # Holding user - only sees Group menu
150
+ if role == UserRole.HOLDING:
151
+ st.markdown("##### Espace Groupe")
152
+ page3 = option_menu(
153
+ menu_title=None,
154
+ options=["Tableau de bord", "Tresorerie", "Investissements", "Assurances", "Filiales", "Consolidation", "Risques", "Gouvernance"],
155
+ icons=["speedometer2", "bank", "graph-up-arrow", "shield", "building", "bar-chart", "shield-exclamation", "people"],
156
+ default_index=0,
157
+ key="menu3",
158
+ styles={
159
+ "container": {"padding": "0!important", "background-color": "transparent"},
160
+ "icon": {"color": "#00ff88", "font-size": "16px"},
161
+ "nav-link": {"font-size": "13px", "text-align": "left", "margin": "2px 0", "padding": "8px 10px", "--hover-color": "#1e1e2f", "border-radius": "6px"},
162
+ "nav-link-selected": {"background-color": "#1b4e2d", "color": "#00ff88"},
163
+ }
164
+ )
165
+ else:
166
+ # Client menu - visible to CLIENT, ADVISOR, DIRECTOR, ADMIN
167
+ st.markdown("##### Espace Client")
168
+ page = option_menu(
169
+ menu_title=None,
170
+ options=["Dashboard", "Comptes", "Virements", "Beneficiaires", "Cartes", "Credits", "Frais", "SEPA"],
171
+ icons=["speedometer2", "wallet2", "arrow-left-right", "people", "credit-card", "cash-stack", "percent", "file-earmark-code"],
172
+ default_index=0,
173
+ styles={
174
+ "container": {"padding": "0!important", "background-color": "transparent"},
175
+ "icon": {"color": "#00d4ff", "font-size": "16px"},
176
+ "nav-link": {"font-size": "13px", "text-align": "left", "margin": "2px 0", "padding": "8px 10px", "--hover-color": "#1e1e2f", "border-radius": "6px"},
177
+ "nav-link-selected": {"background-color": "#1e3a5f", "color": "#00d4ff"},
178
+ }
179
+ )
180
+
181
+ # Bank menu - visible to ADVISOR and above (but not HOLDING)
182
+ if user.can_access(UserRole.ADVISOR) and role != UserRole.HOLDING:
183
+ st.divider()
184
+ st.markdown("##### Espace Banque")
185
+
186
+ bank_options = ["Conseiller"]
187
+ bank_icons = ["person-badge"]
188
+
189
+ if user.can_access(UserRole.DIRECTOR):
190
+ bank_options.extend(["Agences", "Audit"])
191
+ bank_icons.extend(["building", "shield-check"])
192
+
193
+ if user.can_access(UserRole.ADMIN):
194
+ bank_options.append("Administration")
195
+ bank_icons.append("gear")
196
+
197
+ page2 = option_menu(
198
+ menu_title=None,
199
+ options=bank_options,
200
+ icons=bank_icons,
201
+ default_index=0,
202
+ key="menu2",
203
+ styles={
204
+ "container": {"padding": "0!important", "background-color": "transparent"},
205
+ "icon": {"color": "#7b2cbf", "font-size": "16px"},
206
+ "nav-link": {"font-size": "13px", "text-align": "left", "margin": "2px 0", "padding": "8px 10px", "--hover-color": "#1e1e2f", "border-radius": "6px"},
207
+ "nav-link-selected": {"background-color": "#2d1b4e", "color": "#7b2cbf"},
208
+ }
209
+ )
210
+
211
+ st.divider()
212
+
213
+ # Quick stats
214
+ bank = st.session_state.bank
215
+ stats = bank.get_stats()
216
+ col1, col2 = st.columns(2)
217
+ with col1:
218
+ st.metric("Comptes", stats["total_accounts"])
219
+ with col2:
220
+ st.metric("Transactions", stats["total_transactions"])
221
+
222
+ return page, page2, page3
223
+
224
+
225
+ def main():
226
+ """Main app entry point"""
227
+ init_session_state()
228
+
229
+ # Check if logged in
230
+ if not st.session_state.logged_in:
231
+ render_login()
232
+ return
233
+
234
+ page, page2, page3 = render_sidebar()
235
+ user = st.session_state.user
236
+
237
+ # Map French menu to render functions
238
+ client_pages = {
239
+ "Dashboard": render_dashboard,
240
+ "Comptes": render_accounts,
241
+ "Virements": render_transfers,
242
+ "Beneficiaires": render_beneficiaries,
243
+ "Cartes": render_cards,
244
+ "Credits": render_loans,
245
+ "Frais": render_fees,
246
+ "SEPA": render_sepa,
247
+ }
248
+
249
+ bank_pages = {
250
+ "Conseiller": render_advisor,
251
+ "Agences": render_branches,
252
+ "Audit": render_audit,
253
+ "Administration": render_settings,
254
+ }
255
+
256
+ # Holding has separate pages per tab
257
+ group_pages = {
258
+ "Tableau de bord": "dashboard",
259
+ "Tresorerie": "tresorerie",
260
+ "Investissements": "investissements",
261
+ "Assurances": "assurances",
262
+ "Filiales": "filiales",
263
+ "Consolidation": "consolidation",
264
+ "Risques": "risques",
265
+ "Gouvernance": "gouvernance",
266
+ }
267
+
268
+ # Determine active page from menus
269
+ active_page = None
270
+ if page3 and page3 in group_pages:
271
+ # Holding user - render holding page with selected tab
272
+ render_holding(tab=group_pages[page3])
273
+ elif page2 and page2 in bank_pages:
274
+ active_page = page2
275
+ bank_pages[active_page]()
276
+ elif page and page in client_pages:
277
+ active_page = page
278
+ client_pages[active_page]()
279
+
280
+
281
+ if __name__ == "__main__":
282
+ main()