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,326 @@
1
+ """
2
+ SQLite Storage - Database persistence for banking data
3
+ """
4
+
5
+ import sqlite3
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any, Optional, Dict, List
9
+ from datetime import datetime
10
+ from decimal import Decimal
11
+ from contextlib import contextmanager
12
+
13
+
14
+ class SQLiteStorage:
15
+ """
16
+ SQLite database storage for banking data
17
+ """
18
+
19
+ def __init__(self, data_dir: Optional[str] = None):
20
+ self.data_dir = Path(data_dir) if data_dir else Path.home() / ".nanopy-bank"
21
+ self.data_dir.mkdir(parents=True, exist_ok=True)
22
+ self.db_path = self.data_dir / "bank.db"
23
+ self._init_db()
24
+
25
+ def _init_db(self):
26
+ """Initialize database tables"""
27
+ with self._get_connection() as conn:
28
+ cursor = conn.cursor()
29
+
30
+ # Customers table
31
+ cursor.execute("""
32
+ CREATE TABLE IF NOT EXISTS customers (
33
+ customer_id TEXT PRIMARY KEY,
34
+ first_name TEXT,
35
+ last_name TEXT,
36
+ email TEXT,
37
+ phone TEXT,
38
+ address TEXT,
39
+ city TEXT,
40
+ postal_code TEXT,
41
+ country TEXT DEFAULT 'FR',
42
+ birth_date TEXT,
43
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
44
+ data JSON
45
+ )
46
+ """)
47
+
48
+ # Accounts table
49
+ cursor.execute("""
50
+ CREATE TABLE IF NOT EXISTS accounts (
51
+ iban TEXT PRIMARY KEY,
52
+ bic TEXT DEFAULT 'NANPFRPP',
53
+ account_type TEXT DEFAULT 'checking',
54
+ currency TEXT DEFAULT 'EUR',
55
+ balance TEXT DEFAULT '0.00',
56
+ available_balance TEXT DEFAULT '0.00',
57
+ overdraft_limit TEXT DEFAULT '0.00',
58
+ customer_id TEXT,
59
+ account_name TEXT,
60
+ status TEXT DEFAULT 'active',
61
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
62
+ last_transaction TEXT,
63
+ data JSON,
64
+ FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
65
+ )
66
+ """)
67
+
68
+ # Transactions table
69
+ cursor.execute("""
70
+ CREATE TABLE IF NOT EXISTS transactions (
71
+ transaction_id TEXT PRIMARY KEY,
72
+ reference TEXT,
73
+ transaction_type TEXT,
74
+ amount TEXT,
75
+ currency TEXT DEFAULT 'EUR',
76
+ from_iban TEXT,
77
+ to_iban TEXT,
78
+ account_iban TEXT,
79
+ label TEXT,
80
+ description TEXT,
81
+ category TEXT,
82
+ counterparty_name TEXT,
83
+ counterparty_iban TEXT,
84
+ counterparty_bic TEXT,
85
+ status TEXT DEFAULT 'completed',
86
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
87
+ executed_at TEXT,
88
+ value_date TEXT,
89
+ end_to_end_id TEXT,
90
+ mandate_id TEXT,
91
+ balance_after TEXT,
92
+ data JSON,
93
+ FOREIGN KEY (account_iban) REFERENCES accounts(iban)
94
+ )
95
+ """)
96
+
97
+ # Cards table
98
+ cursor.execute("""
99
+ CREATE TABLE IF NOT EXISTS cards (
100
+ card_id TEXT PRIMARY KEY,
101
+ card_number TEXT,
102
+ card_type TEXT DEFAULT 'visa',
103
+ expiry_month INTEGER,
104
+ expiry_year INTEGER,
105
+ status TEXT DEFAULT 'active',
106
+ account_iban TEXT,
107
+ daily_limit TEXT DEFAULT '1000.00',
108
+ monthly_limit TEXT DEFAULT '5000.00',
109
+ contactless_enabled INTEGER DEFAULT 1,
110
+ online_payments_enabled INTEGER DEFAULT 1,
111
+ data JSON,
112
+ FOREIGN KEY (account_iban) REFERENCES accounts(iban)
113
+ )
114
+ """)
115
+
116
+ # Create indexes
117
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_transactions_account ON transactions(account_iban)")
118
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_transactions_date ON transactions(created_at)")
119
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_accounts_customer ON accounts(customer_id)")
120
+
121
+ conn.commit()
122
+
123
+ @contextmanager
124
+ def _get_connection(self):
125
+ """Get database connection with context manager"""
126
+ conn = sqlite3.connect(str(self.db_path))
127
+ conn.row_factory = sqlite3.Row
128
+ try:
129
+ yield conn
130
+ finally:
131
+ conn.close()
132
+
133
+ # ========== CUSTOMERS ==========
134
+
135
+ def save_customer(self, customer: Dict):
136
+ """Save or update customer"""
137
+ with self._get_connection() as conn:
138
+ cursor = conn.cursor()
139
+ cursor.execute("""
140
+ INSERT OR REPLACE INTO customers
141
+ (customer_id, first_name, last_name, email, phone, address, city, postal_code, country, birth_date, created_at, data)
142
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
143
+ """, (
144
+ customer.get("customer_id"),
145
+ customer.get("first_name"),
146
+ customer.get("last_name"),
147
+ customer.get("email"),
148
+ customer.get("phone"),
149
+ customer.get("address"),
150
+ customer.get("city"),
151
+ customer.get("postal_code"),
152
+ customer.get("country", "FR"),
153
+ customer.get("birth_date"),
154
+ customer.get("created_at", datetime.now().isoformat()),
155
+ json.dumps(customer)
156
+ ))
157
+ conn.commit()
158
+
159
+ def get_customer(self, customer_id: str) -> Optional[Dict]:
160
+ """Get customer by ID"""
161
+ with self._get_connection() as conn:
162
+ cursor = conn.cursor()
163
+ cursor.execute("SELECT data FROM customers WHERE customer_id = ?", (customer_id,))
164
+ row = cursor.fetchone()
165
+ return json.loads(row["data"]) if row else None
166
+
167
+ def get_all_customers(self) -> List[Dict]:
168
+ """Get all customers"""
169
+ with self._get_connection() as conn:
170
+ cursor = conn.cursor()
171
+ cursor.execute("SELECT data FROM customers ORDER BY created_at DESC")
172
+ return [json.loads(row["data"]) for row in cursor.fetchall()]
173
+
174
+ # ========== ACCOUNTS ==========
175
+
176
+ def save_account(self, account: Dict):
177
+ """Save or update account"""
178
+ with self._get_connection() as conn:
179
+ cursor = conn.cursor()
180
+ cursor.execute("""
181
+ INSERT OR REPLACE INTO accounts
182
+ (iban, bic, account_type, currency, balance, available_balance, overdraft_limit,
183
+ customer_id, account_name, status, created_at, last_transaction, data)
184
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
185
+ """, (
186
+ account.get("iban"),
187
+ account.get("bic", "NANPFRPP"),
188
+ account.get("account_type", "checking"),
189
+ account.get("currency", "EUR"),
190
+ str(account.get("balance", "0.00")),
191
+ str(account.get("available_balance", "0.00")),
192
+ str(account.get("overdraft_limit", "0.00")),
193
+ account.get("customer_id"),
194
+ account.get("account_name"),
195
+ account.get("status", "active"),
196
+ account.get("created_at", datetime.now().isoformat()),
197
+ account.get("last_transaction"),
198
+ json.dumps(account)
199
+ ))
200
+ conn.commit()
201
+
202
+ def get_account(self, iban: str) -> Optional[Dict]:
203
+ """Get account by IBAN"""
204
+ iban = iban.replace(" ", "").upper()
205
+ with self._get_connection() as conn:
206
+ cursor = conn.cursor()
207
+ cursor.execute("SELECT data FROM accounts WHERE iban = ?", (iban,))
208
+ row = cursor.fetchone()
209
+ return json.loads(row["data"]) if row else None
210
+
211
+ def get_all_accounts(self) -> List[Dict]:
212
+ """Get all accounts"""
213
+ with self._get_connection() as conn:
214
+ cursor = conn.cursor()
215
+ cursor.execute("SELECT data FROM accounts ORDER BY created_at DESC")
216
+ return [json.loads(row["data"]) for row in cursor.fetchall()]
217
+
218
+ def get_customer_accounts(self, customer_id: str) -> List[Dict]:
219
+ """Get accounts for a customer"""
220
+ with self._get_connection() as conn:
221
+ cursor = conn.cursor()
222
+ cursor.execute("SELECT data FROM accounts WHERE customer_id = ?", (customer_id,))
223
+ return [json.loads(row["data"]) for row in cursor.fetchall()]
224
+
225
+ # ========== TRANSACTIONS ==========
226
+
227
+ def save_transaction(self, transaction: Dict):
228
+ """Save transaction"""
229
+ with self._get_connection() as conn:
230
+ cursor = conn.cursor()
231
+ cursor.execute("""
232
+ INSERT OR REPLACE INTO transactions
233
+ (transaction_id, reference, transaction_type, amount, currency, from_iban, to_iban,
234
+ account_iban, label, description, category, counterparty_name, counterparty_iban,
235
+ counterparty_bic, status, created_at, executed_at, value_date, end_to_end_id,
236
+ mandate_id, balance_after, data)
237
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
238
+ """, (
239
+ transaction.get("transaction_id"),
240
+ transaction.get("reference"),
241
+ transaction.get("transaction_type"),
242
+ str(transaction.get("amount", "0.00")),
243
+ transaction.get("currency", "EUR"),
244
+ transaction.get("from_iban"),
245
+ transaction.get("to_iban"),
246
+ transaction.get("account_iban"),
247
+ transaction.get("label"),
248
+ transaction.get("description"),
249
+ transaction.get("category"),
250
+ transaction.get("counterparty_name"),
251
+ transaction.get("counterparty_iban"),
252
+ transaction.get("counterparty_bic"),
253
+ transaction.get("status", "completed"),
254
+ transaction.get("created_at", datetime.now().isoformat()),
255
+ transaction.get("executed_at"),
256
+ transaction.get("value_date"),
257
+ transaction.get("end_to_end_id"),
258
+ transaction.get("mandate_id"),
259
+ str(transaction.get("balance_after", "")) if transaction.get("balance_after") else None,
260
+ json.dumps(transaction)
261
+ ))
262
+ conn.commit()
263
+
264
+ def get_transaction(self, transaction_id: str) -> Optional[Dict]:
265
+ """Get transaction by ID"""
266
+ with self._get_connection() as conn:
267
+ cursor = conn.cursor()
268
+ cursor.execute("SELECT data FROM transactions WHERE transaction_id = ?", (transaction_id,))
269
+ row = cursor.fetchone()
270
+ return json.loads(row["data"]) if row else None
271
+
272
+ def get_account_transactions(self, iban: str, limit: int = 50) -> List[Dict]:
273
+ """Get transactions for an account"""
274
+ iban = iban.replace(" ", "").upper()
275
+ with self._get_connection() as conn:
276
+ cursor = conn.cursor()
277
+ cursor.execute(
278
+ "SELECT data FROM transactions WHERE account_iban = ? ORDER BY created_at DESC LIMIT ?",
279
+ (iban, limit)
280
+ )
281
+ return [json.loads(row["data"]) for row in cursor.fetchall()]
282
+
283
+ def get_all_transactions(self, limit: int = 100) -> List[Dict]:
284
+ """Get all transactions"""
285
+ with self._get_connection() as conn:
286
+ cursor = conn.cursor()
287
+ cursor.execute("SELECT data FROM transactions ORDER BY created_at DESC LIMIT ?", (limit,))
288
+ return [json.loads(row["data"]) for row in cursor.fetchall()]
289
+
290
+ # ========== STATS ==========
291
+
292
+ def get_stats(self) -> Dict:
293
+ """Get database statistics"""
294
+ with self._get_connection() as conn:
295
+ cursor = conn.cursor()
296
+
297
+ cursor.execute("SELECT COUNT(*) FROM customers")
298
+ customer_count = cursor.fetchone()[0]
299
+
300
+ cursor.execute("SELECT COUNT(*) FROM accounts")
301
+ account_count = cursor.fetchone()[0]
302
+
303
+ cursor.execute("SELECT COUNT(*) FROM transactions")
304
+ transaction_count = cursor.fetchone()[0]
305
+
306
+ cursor.execute("SELECT SUM(CAST(balance AS REAL)) FROM accounts")
307
+ total_balance = cursor.fetchone()[0] or 0
308
+
309
+ return {
310
+ "customers": customer_count,
311
+ "accounts": account_count,
312
+ "transactions": transaction_count,
313
+ "total_balance": f"{total_balance:.2f}",
314
+ }
315
+
316
+
317
+ # Singleton
318
+ _sqlite_instance: Optional[SQLiteStorage] = None
319
+
320
+
321
+ def get_sqlite_storage(data_dir: Optional[str] = None) -> SQLiteStorage:
322
+ """Get or create SQLite storage instance"""
323
+ global _sqlite_instance
324
+ if _sqlite_instance is None:
325
+ _sqlite_instance = SQLiteStorage(data_dir)
326
+ return _sqlite_instance
@@ -0,0 +1,14 @@
1
+ """
2
+ UI Module - Streamlit application
3
+ """
4
+
5
+ from .pages import dashboard, accounts, transfers, cards, sepa, settings
6
+
7
+ __all__ = [
8
+ "dashboard",
9
+ "accounts",
10
+ "transfers",
11
+ "cards",
12
+ "sepa",
13
+ "settings",
14
+ ]
@@ -0,0 +1,33 @@
1
+ """
2
+ UI Pages - Each page in its own module
3
+ """
4
+
5
+ from .dashboard import render_dashboard
6
+ from .accounts import render_accounts
7
+ from .transfers import render_transfers
8
+ from .beneficiaries import render_beneficiaries
9
+ from .cards import render_cards
10
+ from .loans import render_loans
11
+ from .fees import render_fees
12
+ from .branches import render_branches
13
+ from .sepa import render_sepa
14
+ from .audit import render_audit
15
+ from .settings import render_settings
16
+ from .advisor import render_advisor
17
+ from .holding import render_holding
18
+
19
+ __all__ = [
20
+ "render_dashboard",
21
+ "render_accounts",
22
+ "render_transfers",
23
+ "render_beneficiaries",
24
+ "render_cards",
25
+ "render_loans",
26
+ "render_fees",
27
+ "render_branches",
28
+ "render_sepa",
29
+ "render_audit",
30
+ "render_settings",
31
+ "render_advisor",
32
+ "render_holding",
33
+ ]
@@ -0,0 +1,85 @@
1
+ """
2
+ Accounts page
3
+ """
4
+
5
+ import streamlit as st
6
+ from decimal import Decimal
7
+
8
+ from .common import format_currency, page_header
9
+
10
+
11
+ def render_accounts():
12
+ """Render accounts page"""
13
+ page_header("Accounts")
14
+
15
+ bank = st.session_state.bank
16
+
17
+ if bank.accounts:
18
+ st.markdown("### Your Accounts")
19
+
20
+ for iban, account in bank.accounts.items():
21
+ with st.expander(f"💳 {account.account_name} - {account.format_iban()}", expanded=False):
22
+ col1, col2 = st.columns(2)
23
+ with col1:
24
+ st.markdown(f"**Balance:** {format_currency(account.balance, account.currency.value)}")
25
+ st.markdown(f"**Available:** {format_currency(account.available_balance, account.currency.value)}")
26
+ st.markdown(f"**Type:** {account.account_type.value.title()}")
27
+ with col2:
28
+ st.markdown(f"**IBAN:** `{account.format_iban()}`")
29
+ st.markdown(f"**BIC:** `{account.bic}`")
30
+ st.markdown(f"**Status:** {account.status.value.title()}")
31
+
32
+ st.divider()
33
+
34
+ st.markdown("### Create New Account")
35
+
36
+ with st.form("create_account_form"):
37
+ col1, col2 = st.columns(2)
38
+
39
+ with col1:
40
+ if bank.customers:
41
+ customer_options = {f"{c.full_name} ({c.customer_id})": c.customer_id for c in bank.customers.values()}
42
+ customer_select = st.selectbox("Select Customer", options=["New Customer"] + list(customer_options.keys()))
43
+ else:
44
+ customer_select = "New Customer"
45
+
46
+ if customer_select == "New Customer":
47
+ first_name = st.text_input("First Name")
48
+ last_name = st.text_input("Last Name")
49
+ email = st.text_input("Email")
50
+ else:
51
+ first_name = last_name = email = None
52
+ selected_customer_id = customer_options[customer_select]
53
+
54
+ with col2:
55
+ from nanopy_bank.core import AccountType, Currency
56
+
57
+ account_name = st.text_input("Account Name", "Mon Compte")
58
+ account_type = st.selectbox("Account Type", [at.value for at in AccountType])
59
+ currency = st.selectbox("Currency", [c.value for c in Currency])
60
+ initial_balance = st.number_input("Initial Balance", min_value=0.0, value=0.0, step=100.0)
61
+
62
+ submitted = st.form_submit_button("Create Account")
63
+
64
+ if submitted:
65
+ try:
66
+ if customer_select == "New Customer":
67
+ if not first_name or not last_name or not email:
68
+ st.error("Please fill in all customer fields")
69
+ else:
70
+ customer = bank.create_customer(first_name, last_name, email)
71
+ selected_customer_id = customer.customer_id
72
+ else:
73
+ selected_customer_id = customer_options[customer_select]
74
+
75
+ account = bank.create_account(
76
+ customer_id=selected_customer_id,
77
+ account_type=AccountType(account_type),
78
+ currency=Currency(currency),
79
+ initial_balance=Decimal(str(initial_balance)),
80
+ account_name=account_name
81
+ )
82
+ st.success(f"Account created: {account.format_iban()}")
83
+ st.rerun()
84
+ except Exception as e:
85
+ st.error(f"Error: {e}")
@@ -0,0 +1,140 @@
1
+ """
2
+ Advisor page - Employee/Conseiller view
3
+ """
4
+
5
+ import streamlit as st
6
+ import pandas as pd
7
+
8
+ from .common import page_header, format_currency
9
+
10
+
11
+ def render_advisor():
12
+ """Render advisor dashboard"""
13
+ page_header("Espace Conseiller")
14
+
15
+ bank = st.session_state.bank
16
+
17
+ tab1, tab2, tab3, tab4 = st.tabs(["Mes Clients", "Demandes", "Objectifs", "Agenda"])
18
+
19
+ with tab1:
20
+ st.markdown("### Portefeuille Clients")
21
+
22
+ col1, col2, col3 = st.columns(3)
23
+ with col1:
24
+ st.metric("Clients geres", len(bank.customers) or 42)
25
+ with col2:
26
+ st.metric("Encours total", "2.4M EUR")
27
+ with col3:
28
+ st.metric("RDV ce mois", 18)
29
+
30
+ st.divider()
31
+
32
+ # Client list
33
+ clients = [
34
+ {"name": "Jean Dupont", "status": "Premium", "balance": "45,000 EUR", "products": 4, "last_contact": "02/01/2026"},
35
+ {"name": "Marie Martin", "status": "Standard", "balance": "12,500 EUR", "products": 2, "last_contact": "28/12/2025"},
36
+ {"name": "Pierre Bernard", "status": "Pro", "balance": "85,000 EUR", "products": 6, "last_contact": "05/01/2026"},
37
+ {"name": "Sophie Petit", "status": "Jeune", "balance": "2,300 EUR", "products": 1, "last_contact": "15/12/2025"},
38
+ ]
39
+
40
+ for client in clients:
41
+ col1, col2, col3, col4, col5 = st.columns([2, 1, 2, 1, 1])
42
+ with col1:
43
+ st.markdown(f"**{client['name']}**")
44
+ with col2:
45
+ if client['status'] == 'Premium':
46
+ st.success(client['status'])
47
+ elif client['status'] == 'Pro':
48
+ st.info(client['status'])
49
+ else:
50
+ st.caption(client['status'])
51
+ with col3:
52
+ st.markdown(f":green[{client['balance']}]")
53
+ with col4:
54
+ st.caption(f"{client['products']} produits")
55
+ with col5:
56
+ st.button("Fiche", key=f"client_{client['name']}")
57
+ st.divider()
58
+
59
+ with tab2:
60
+ st.markdown("### Demandes en attente")
61
+
62
+ requests = [
63
+ {"client": "Jean Dupont", "type": "Pret personnel", "amount": "15,000 EUR", "date": "04/01/2026", "priority": "High"},
64
+ {"client": "Pierre Bernard", "type": "Augmentation decouvert", "amount": "5,000 EUR", "date": "03/01/2026", "priority": "Medium"},
65
+ {"client": "Marie Martin", "type": "Carte Gold", "amount": "-", "date": "02/01/2026", "priority": "Low"},
66
+ ]
67
+
68
+ for req in requests:
69
+ col1, col2, col3, col4, col5 = st.columns([2, 2, 2, 1, 2])
70
+ with col1:
71
+ st.markdown(f"**{req['client']}**")
72
+ st.caption(req['type'])
73
+ with col2:
74
+ st.markdown(req['amount'])
75
+ with col3:
76
+ st.caption(req['date'])
77
+ with col4:
78
+ if req['priority'] == 'High':
79
+ st.error("Urgent")
80
+ elif req['priority'] == 'Medium':
81
+ st.warning("Normal")
82
+ else:
83
+ st.info("Faible")
84
+ with col5:
85
+ c1, c2 = st.columns(2)
86
+ with c1:
87
+ st.button("Approuver", key=f"approve_{req['client']}", type="primary")
88
+ with c2:
89
+ st.button("Refuser", key=f"reject_{req['client']}")
90
+ st.divider()
91
+
92
+ with tab3:
93
+ st.markdown("### Objectifs Commerciaux")
94
+
95
+ objectives = [
96
+ {"name": "Ouvertures de compte", "current": 12, "target": 15, "percent": 80},
97
+ {"name": "Credits accordes", "current": 450000, "target": 500000, "percent": 90},
98
+ {"name": "Assurances vendues", "current": 8, "target": 12, "percent": 67},
99
+ {"name": "Cartes Premium", "current": 5, "target": 8, "percent": 62},
100
+ ]
101
+
102
+ for obj in objectives:
103
+ st.markdown(f"**{obj['name']}**")
104
+ st.progress(obj['percent'] / 100)
105
+ st.caption(f"{obj['current']:,} / {obj['target']:,} ({obj['percent']}%)")
106
+ st.divider()
107
+
108
+ with tab4:
109
+ st.markdown("### Agenda")
110
+
111
+ appointments = [
112
+ {"time": "09:00", "client": "Jean Dupont", "type": "Revue annuelle", "location": "Agence"},
113
+ {"time": "11:00", "client": "Marie Martin", "type": "Simulation pret", "location": "Telephone"},
114
+ {"time": "14:30", "client": "Pierre Bernard", "type": "Bilan pro", "location": "Agence"},
115
+ {"time": "16:00", "client": "Sophie Petit", "type": "Premier RDV", "location": "Agence"},
116
+ ]
117
+
118
+ for apt in appointments:
119
+ col1, col2, col3, col4 = st.columns([1, 2, 2, 1])
120
+ with col1:
121
+ st.markdown(f"**{apt['time']}**")
122
+ with col2:
123
+ st.markdown(apt['client'])
124
+ with col3:
125
+ st.caption(apt['type'])
126
+ with col4:
127
+ st.caption(apt['location'])
128
+ st.divider()
129
+
130
+ with st.expander("Planifier un RDV"):
131
+ with st.form("new_appointment"):
132
+ col1, col2 = st.columns(2)
133
+ with col1:
134
+ st.date_input("Date")
135
+ st.time_input("Heure")
136
+ with col2:
137
+ st.selectbox("Client", ["Jean Dupont", "Marie Martin", "Pierre Bernard", "Sophie Petit"])
138
+ st.selectbox("Type", ["Revue annuelle", "Simulation pret", "Premier RDV", "Reclamation"])
139
+ if st.form_submit_button("Planifier"):
140
+ st.success("RDV planifie!")
@@ -0,0 +1,73 @@
1
+ """
2
+ Audit page
3
+ """
4
+
5
+ import streamlit as st
6
+
7
+ from .common import page_header
8
+
9
+
10
+ def render_audit():
11
+ """Render audit logs page"""
12
+ page_header("Security & Audit")
13
+
14
+ tab1, tab2 = st.tabs(["Activity Log", "Security Alerts"])
15
+
16
+ with tab1:
17
+ st.markdown("### Recent Activity")
18
+
19
+ logs = [
20
+ {"time": "05/01/2026 14:32", "action": "Login", "details": "Successful login from Paris, FR", "ip": "86.123.45.67"},
21
+ {"time": "05/01/2026 14:35", "action": "Transfer", "details": "SEPA transfer of 150.00 EUR", "ip": "86.123.45.67"},
22
+ {"time": "05/01/2026 14:40", "action": "Card Block", "details": "Temporary card block activated", "ip": "86.123.45.67"},
23
+ {"time": "04/01/2026 09:15", "action": "Login", "details": "Successful login from Lyon, FR", "ip": "92.184.12.34"},
24
+ {"time": "03/01/2026 18:22", "action": "Beneficiary", "details": "New beneficiary added", "ip": "86.123.45.67"},
25
+ ]
26
+
27
+ for log in logs:
28
+ col1, col2, col3 = st.columns([2, 4, 2])
29
+ with col1:
30
+ st.caption(log['time'])
31
+ with col2:
32
+ st.markdown(f"**{log['action']}**")
33
+ st.caption(log['details'])
34
+ with col3:
35
+ st.caption(f"IP: {log['ip']}")
36
+ st.divider()
37
+
38
+ with tab2:
39
+ st.markdown("### Security Alerts")
40
+
41
+ alerts = [
42
+ {"time": "02/01/2026 22:45", "type": "Unusual Location", "desc": "Login attempt from Germany", "status": "Blocked", "risk": "High"},
43
+ {"time": "28/12/2025 15:30", "type": "Large Transaction", "desc": "Transfer > 5,000 EUR detected", "status": "Verified", "risk": "Medium"},
44
+ ]
45
+
46
+ for alert in alerts:
47
+ col1, col2, col3, col4 = st.columns([2, 3, 2, 1])
48
+ with col1:
49
+ st.caption(alert['time'])
50
+ if alert['risk'] == 'High':
51
+ st.error(f"Risk: {alert['risk']}")
52
+ else:
53
+ st.warning(f"Risk: {alert['risk']}")
54
+ with col2:
55
+ st.markdown(f"**{alert['type']}**")
56
+ st.caption(alert['desc'])
57
+ with col3:
58
+ if alert['status'] == 'Blocked':
59
+ st.error(alert['status'])
60
+ else:
61
+ st.success(alert['status'])
62
+ with col4:
63
+ st.button("Details", key=f"alert_{alert['time']}")
64
+ st.divider()
65
+
66
+ st.markdown("### Security Settings")
67
+ col1, col2 = st.columns(2)
68
+ with col1:
69
+ st.toggle("Two-Factor Authentication", value=True)
70
+ st.toggle("Login Notifications", value=True)
71
+ with col2:
72
+ st.toggle("Transaction Alerts", value=True)
73
+ st.toggle("Unusual Activity Alerts", value=True)