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.
- nanopy_bank/__init__.py +20 -0
- nanopy_bank/api/__init__.py +10 -0
- nanopy_bank/api/server.py +242 -0
- nanopy_bank/app.py +282 -0
- nanopy_bank/cli.py +152 -0
- nanopy_bank/core/__init__.py +85 -0
- nanopy_bank/core/audit.py +404 -0
- nanopy_bank/core/auth.py +306 -0
- nanopy_bank/core/bank.py +407 -0
- nanopy_bank/core/beneficiary.py +258 -0
- nanopy_bank/core/branch.py +319 -0
- nanopy_bank/core/fees.py +243 -0
- nanopy_bank/core/holding.py +416 -0
- nanopy_bank/core/models.py +308 -0
- nanopy_bank/core/products.py +300 -0
- nanopy_bank/data/__init__.py +31 -0
- nanopy_bank/data/demo.py +846 -0
- nanopy_bank/documents/__init__.py +11 -0
- nanopy_bank/documents/statement.py +304 -0
- nanopy_bank/sepa/__init__.py +10 -0
- nanopy_bank/sepa/sepa.py +452 -0
- nanopy_bank/storage/__init__.py +11 -0
- nanopy_bank/storage/json_storage.py +127 -0
- nanopy_bank/storage/sqlite_storage.py +326 -0
- nanopy_bank/ui/__init__.py +14 -0
- nanopy_bank/ui/pages/__init__.py +33 -0
- nanopy_bank/ui/pages/accounts.py +85 -0
- nanopy_bank/ui/pages/advisor.py +140 -0
- nanopy_bank/ui/pages/audit.py +73 -0
- nanopy_bank/ui/pages/beneficiaries.py +115 -0
- nanopy_bank/ui/pages/branches.py +64 -0
- nanopy_bank/ui/pages/cards.py +36 -0
- nanopy_bank/ui/pages/common.py +18 -0
- nanopy_bank/ui/pages/dashboard.py +100 -0
- nanopy_bank/ui/pages/fees.py +60 -0
- nanopy_bank/ui/pages/holding.py +943 -0
- nanopy_bank/ui/pages/loans.py +105 -0
- nanopy_bank/ui/pages/login.py +174 -0
- nanopy_bank/ui/pages/sepa.py +118 -0
- nanopy_bank/ui/pages/settings.py +48 -0
- nanopy_bank/ui/pages/transfers.py +94 -0
- nanopy_bank/ui/pages.py +16 -0
- nanopy_bank-1.0.8.dist-info/METADATA +72 -0
- nanopy_bank-1.0.8.dist-info/RECORD +47 -0
- nanopy_bank-1.0.8.dist-info/WHEEL +5 -0
- nanopy_bank-1.0.8.dist-info/entry_points.txt +2 -0
- nanopy_bank-1.0.8.dist-info/top_level.txt +1 -0
nanopy_bank/__init__.py
ADDED
|
@@ -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,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()
|