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,943 @@
1
+ """
2
+ Holding page - Group management view for Nova x Genesis SASU
3
+ """
4
+
5
+ import streamlit as st
6
+ from decimal import Decimal
7
+ from datetime import datetime, date
8
+
9
+ from .common import page_header
10
+
11
+ try:
12
+ from ...data.demo import get_demo_holding_data
13
+ except ImportError:
14
+ from nanopy_bank.data.demo import get_demo_holding_data
15
+
16
+
17
+ def get_holding_data():
18
+ """Get or create holding data in session state"""
19
+ if "holding_data" not in st.session_state:
20
+ st.session_state.holding_data = get_demo_holding_data()
21
+ return st.session_state.holding_data
22
+
23
+
24
+ def format_amount(amount: Decimal, currency: str = "EUR") -> str:
25
+ """Format amount with thousands separator"""
26
+ return f"{amount:,.2f} {currency}".replace(",", " ").replace(".", ",").replace(" ", " ")
27
+
28
+
29
+ def render_holding(tab: str = "dashboard"):
30
+ """Render holding/group management dashboard"""
31
+ data = get_holding_data()
32
+ holding = data["holding"]
33
+
34
+ page_header(holding["name"])
35
+
36
+ st.markdown(f"""
37
+ <div style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border: 1px solid #333; border-radius: 12px; padding: 20px; margin-bottom: 20px;">
38
+ <div style="color: #00ff88; font-size: 12px; text-transform: uppercase;">{holding['legal_name']}</div>
39
+ <div style="color: white; font-size: 24px; font-weight: bold; margin: 8px 0;">SIREN: {holding['siren']} | LEI: {holding['lei']}</div>
40
+ </div>
41
+ """, unsafe_allow_html=True)
42
+
43
+ if tab == "dashboard":
44
+ render_dashboard_tab(data)
45
+ elif tab == "tresorerie":
46
+ render_tresorerie_tab(data)
47
+ elif tab == "investissements":
48
+ render_investissements_tab(data)
49
+ elif tab == "assurances":
50
+ render_assurances_tab(data)
51
+ elif tab == "filiales":
52
+ render_filiales_tab(data)
53
+ elif tab == "consolidation":
54
+ render_consolidation_tab(data)
55
+ elif tab == "risques":
56
+ render_risques_tab(data)
57
+ elif tab == "gouvernance":
58
+ render_gouvernance_tab(data)
59
+
60
+
61
+ def render_dashboard_tab(data):
62
+ """Dashboard with KPIs"""
63
+ st.markdown("### Vue d'ensemble")
64
+
65
+ # Calculate totals
66
+ total_treasury = data["accounts"]["principal"]["balance"] + data["accounts"]["tresorerie"]["balance"]
67
+ total_assets = sum(s["assets"] for s in data["subsidiaries"])
68
+ total_employees = sum(s["employees"] for s in data["subsidiaries"])
69
+ pool_net = sum(s["pool_balance"] for s in data["subsidiaries"])
70
+
71
+ col1, col2, col3, col4 = st.columns(4)
72
+ with col1:
73
+ st.metric("Tresorerie Holding", format_amount(total_treasury))
74
+ with col2:
75
+ st.metric("Actifs Consolides", f"{total_assets/1000000000:.1f}B EUR")
76
+ with col3:
77
+ st.metric("Effectif Groupe", f"{total_employees:,}".replace(",", " "))
78
+ with col4:
79
+ st.metric("Position Cash Pool", format_amount(pool_net))
80
+
81
+ st.divider()
82
+
83
+ # Recent activity
84
+ col1, col2 = st.columns(2)
85
+
86
+ with col1:
87
+ st.markdown("### Filiales")
88
+ for sub in data["subsidiaries"][:3]:
89
+ status_color = "#00ff88" if sub["status"] == "active" else "#ffd93d"
90
+ st.markdown(f"""
91
+ <div style="background: #1e1e2f; padding: 12px; border-radius: 8px; margin-bottom: 8px;">
92
+ <div style="display: flex; justify-content: space-between;">
93
+ <span style="color: white; font-weight: bold;">{sub['name']}</span>
94
+ <span style="color: {status_color};">{sub['ownership']}%</span>
95
+ </div>
96
+ <div style="color: #888; font-size: 12px;">{sub['type']}</div>
97
+ </div>
98
+ """, unsafe_allow_html=True)
99
+
100
+ with col2:
101
+ st.markdown("### Prets Intra-Groupe")
102
+ for loan in data["intra_group_loans"][:3]:
103
+ st.markdown(f"""
104
+ <div style="background: #1e1e2f; padding: 12px; border-radius: 8px; margin-bottom: 8px;">
105
+ <div style="display: flex; justify-content: space-between;">
106
+ <span style="color: white; font-weight: bold;">{loan['borrower']}</span>
107
+ <span style="color: #00d4ff;">{format_amount(loan['outstanding'])}</span>
108
+ </div>
109
+ <div style="color: #888; font-size: 12px;">Taux: {loan['rate']}% | Echeance: {loan['maturity'].strftime('%d/%m/%Y')}</div>
110
+ </div>
111
+ """, unsafe_allow_html=True)
112
+
113
+
114
+ def render_tresorerie_tab(data):
115
+ """Treasury management"""
116
+ st.markdown("### Comptes Tresorerie")
117
+
118
+ col1, col2 = st.columns(2)
119
+
120
+ for key, account in data["accounts"].items():
121
+ with col1 if key == "principal" else col2:
122
+ color = "#00ff88" if key == "principal" else "#00d4ff"
123
+ st.markdown(f"""
124
+ <div style="background: linear-gradient(135deg, #1b4e2d, #0d2818); border: 1px solid {color}; border-radius: 12px; padding: 20px;">
125
+ <div style="color: {color}; font-size: 12px; text-transform: uppercase;">{account['name']}</div>
126
+ <div style="color: white; font-size: 11px; margin: 4px 0;">{account['iban']}</div>
127
+ <div style="color: {color}; font-size: 28px; font-weight: bold; margin-top: 12px;">{format_amount(account['balance'])}</div>
128
+ </div>
129
+ """, unsafe_allow_html=True)
130
+
131
+ st.divider()
132
+
133
+ # Cash pooling
134
+ st.markdown("### Cash Pooling - Position Nette")
135
+
136
+ for sub in data["subsidiaries"]:
137
+ col1, col2, col3, col4 = st.columns([2, 2, 2, 1])
138
+ with col1:
139
+ st.markdown(f"**{sub['name']}**")
140
+ st.caption(f"FR76 NANP {sub['id'][-4:]} xxxx")
141
+ with col2:
142
+ balance = sub["pool_balance"]
143
+ if balance >= 0:
144
+ st.markdown(f":green[+{format_amount(balance)}]")
145
+ else:
146
+ st.markdown(f":red[{format_amount(balance)}]")
147
+ with col3:
148
+ st.caption(f"Limite: {format_amount(sub['pool_limit'])}")
149
+ with col4:
150
+ if st.button("Virement", key=f"pool_transfer_{sub['id']}"):
151
+ st.session_state.show_transfer_modal = sub['id']
152
+
153
+ # Transfer modal
154
+ if "show_transfer_modal" in st.session_state and st.session_state.show_transfer_modal:
155
+ sub_id = st.session_state.show_transfer_modal
156
+ sub = next((s for s in data["subsidiaries"] if s["id"] == sub_id), None)
157
+ if sub:
158
+ with st.expander(f"Virement vers/depuis {sub['name']}", expanded=True):
159
+ direction = st.radio("Direction", ["Holding -> Filiale", "Filiale -> Holding"], key=f"dir_{sub_id}")
160
+ amount = st.number_input("Montant (EUR)", min_value=0.0, max_value=10000000.0, step=1000.0, key=f"amt_{sub_id}")
161
+ motif = st.text_input("Motif", key=f"motif_{sub_id}")
162
+
163
+ col1, col2 = st.columns(2)
164
+ with col1:
165
+ if st.button("Annuler", key=f"cancel_{sub_id}"):
166
+ del st.session_state.show_transfer_modal
167
+ st.rerun()
168
+ with col2:
169
+ if st.button("Valider", type="primary", key=f"validate_{sub_id}"):
170
+ if amount > 0 and motif:
171
+ # Process transfer
172
+ if "Holding -> Filiale" in direction:
173
+ data["accounts"]["principal"]["balance"] -= Decimal(str(amount))
174
+ sub["pool_balance"] += Decimal(str(amount))
175
+ else:
176
+ data["accounts"]["principal"]["balance"] += Decimal(str(amount))
177
+ sub["pool_balance"] -= Decimal(str(amount))
178
+
179
+ data["transactions"].append({
180
+ "date": datetime.now(),
181
+ "type": "pool_transfer",
182
+ "direction": direction,
183
+ "subsidiary": sub["name"],
184
+ "amount": Decimal(str(amount)),
185
+ "motif": motif,
186
+ })
187
+
188
+ del st.session_state.show_transfer_modal
189
+ st.success(f"Virement de {format_amount(Decimal(str(amount)))} effectue")
190
+ st.rerun()
191
+ else:
192
+ st.error("Veuillez remplir tous les champs")
193
+
194
+ st.divider()
195
+
196
+ pool_net = sum(s["pool_balance"] for s in data["subsidiaries"])
197
+ col1, col2, col3 = st.columns(3)
198
+ with col1:
199
+ st.metric("Position Nette Pool", format_amount(pool_net))
200
+ with col2:
201
+ st.metric("Taux Crediteur", f"{data['pool_rates']['credit']}%")
202
+ with col3:
203
+ st.metric("Taux Debiteur", f"{data['pool_rates']['debit']}%")
204
+
205
+ st.divider()
206
+
207
+ # Intra-group loans
208
+ st.markdown("### Prets Intra-Groupe")
209
+
210
+ for loan in data["intra_group_loans"]:
211
+ col1, col2, col3, col4, col5 = st.columns([2, 2, 1, 2, 1])
212
+ with col1:
213
+ st.markdown(f"**{loan['borrower']}**")
214
+ st.caption(f"Ref: {loan['id']}")
215
+ with col2:
216
+ st.markdown(f":blue[{format_amount(loan['outstanding'])}]")
217
+ st.caption(f"Principal: {format_amount(loan['principal'])}")
218
+ with col3:
219
+ st.caption(f"Taux: {loan['rate']}%")
220
+ with col4:
221
+ st.caption(f"Echeance: {loan['maturity'].strftime('%d/%m/%Y')}")
222
+ with col5:
223
+ if st.button("Details", key=f"loan_{loan['id']}"):
224
+ st.info(f"Debut: {loan['start_date'].strftime('%d/%m/%Y')}\nRembourse: {format_amount(loan['principal'] - loan['outstanding'])}")
225
+ st.divider()
226
+
227
+ total_loans = sum(l["outstanding"] for l in data["intra_group_loans"])
228
+ st.metric("Total Encours Prets", format_amount(total_loans))
229
+
230
+ # New loan button
231
+ if st.button("Nouveau Pret Intra-Groupe", type="primary"):
232
+ st.session_state.show_new_loan = True
233
+
234
+ if st.session_state.get("show_new_loan"):
235
+ with st.expander("Nouveau Pret", expanded=True):
236
+ borrower = st.selectbox("Emprunteur", [s["name"] for s in data["subsidiaries"]])
237
+ principal = st.number_input("Montant (EUR)", min_value=100000.0, max_value=50000000.0, step=100000.0)
238
+ rate = st.number_input("Taux annuel (%)", min_value=0.5, max_value=5.0, step=0.25, value=1.25)
239
+ maturity = st.date_input("Date d'echeance", min_value=date.today())
240
+
241
+ if st.button("Creer le pret", type="primary"):
242
+ sub = next((s for s in data["subsidiaries"] if s["name"] == borrower), None)
243
+ if sub:
244
+ new_loan = {
245
+ "id": f"IGL{len(data['intra_group_loans'])+1:03d}",
246
+ "borrower": borrower,
247
+ "borrower_id": sub["id"],
248
+ "principal": Decimal(str(principal)),
249
+ "outstanding": Decimal(str(principal)),
250
+ "rate": Decimal(str(rate)),
251
+ "start_date": date.today(),
252
+ "maturity": maturity,
253
+ "status": "active",
254
+ }
255
+ data["intra_group_loans"].append(new_loan)
256
+ data["accounts"]["principal"]["balance"] -= Decimal(str(principal))
257
+ del st.session_state.show_new_loan
258
+ st.success(f"Pret de {format_amount(Decimal(str(principal)))} accorde a {borrower}")
259
+ st.rerun()
260
+
261
+
262
+ def render_filiales_tab(data):
263
+ """Subsidiaries management"""
264
+ st.markdown("### Filiales du Groupe")
265
+
266
+ for sub in data["subsidiaries"]:
267
+ with st.expander(f"{sub['name']} ({sub['ownership']}%)", expanded=False):
268
+ col1, col2, col3 = st.columns(3)
269
+ with col1:
270
+ st.markdown(f"**Type:** {sub['type']}")
271
+ st.markdown(f"**Statut:** {'Actif' if sub['status'] == 'active' else 'Startup'}")
272
+ with col2:
273
+ st.markdown(f"**Actifs:** {sub['assets']/1000000:.0f}M EUR")
274
+ st.markdown(f"**Effectif:** {sub['employees']}")
275
+ with col3:
276
+ st.markdown(f"**Participation:** {sub['ownership']}%")
277
+ pool_color = "green" if sub["pool_balance"] >= 0 else "red"
278
+ st.markdown(f"**Position Pool:** :{pool_color}[{format_amount(sub['pool_balance'])}]")
279
+
280
+ # Actions
281
+ col1, col2, col3 = st.columns(3)
282
+ with col1:
283
+ if st.button("Voir details", key=f"details_{sub['id']}"):
284
+ st.info(f"ID: {sub['id']}\nLimite pool: {format_amount(sub['pool_limit'])}")
285
+ with col2:
286
+ if st.button("Historique", key=f"history_{sub['id']}"):
287
+ txs = [t for t in data["transactions"] if t.get("subsidiary") == sub["name"]]
288
+ if txs:
289
+ for tx in txs[-5:]:
290
+ st.write(f"{tx['date'].strftime('%d/%m/%Y')} - {tx['type']} - {format_amount(tx['amount'])}")
291
+ else:
292
+ st.info("Aucune transaction")
293
+
294
+ st.divider()
295
+
296
+ # Dividends
297
+ st.markdown("### Dividendes")
298
+
299
+ for div in data["dividends"]:
300
+ col1, col2, col3, col4, col5 = st.columns([2, 2, 2, 2, 1])
301
+ with col1:
302
+ st.markdown(f"**{div['subsidiary']}**")
303
+ st.caption(f"Exercice {div['year']}")
304
+ with col2:
305
+ st.caption(f"Brut: {format_amount(div['gross'])}")
306
+ with col3:
307
+ st.caption(f"Retenue: {format_amount(div['tax'])}")
308
+ with col4:
309
+ st.markdown(f":green[Net: {format_amount(div['net'])}]")
310
+ with col5:
311
+ status_map = {"paid": ("Paye", "success"), "approved": ("Approuve", "info"), "declared": ("Declare", "warning")}
312
+ label, style = status_map.get(div["status"], ("?", "info"))
313
+ if style == "success":
314
+ st.success(label)
315
+ elif style == "warning":
316
+ st.warning(label)
317
+ else:
318
+ st.info(label)
319
+
320
+ total_div = sum(d["net"] for d in data["dividends"])
321
+ st.metric("Total Dividendes 2025", format_amount(total_div))
322
+
323
+
324
+ def render_investissements_tab(data):
325
+ """Sovereign bonds portfolio management"""
326
+ st.markdown("### Portefeuille Obligataire Souverain")
327
+
328
+ bonds = data.get("sovereign_bonds", [])
329
+ available = data.get("available_bonds", [])
330
+
331
+ # Separate high-yield emerging bonds from core bonds
332
+ emerging_countries = ["UA", "PL", "RO"] # Ukraine, Poland, Romania
333
+ emerging_bonds = [b for b in bonds if b["country"] in emerging_countries]
334
+ core_bonds = [b for b in bonds if b["country"] not in emerging_countries]
335
+
336
+ # Calculate portfolio value
337
+ total_nominal = sum(b["nominal"] for b in bonds) if bonds else Decimal("0")
338
+ total_market_value = sum(b["nominal"] * b["current_price"] / 100 for b in bonds) if bonds else Decimal("0")
339
+ total_cost = sum(b["nominal"] * b["purchase_price"] / 100 for b in bonds) if bonds else Decimal("0")
340
+ pnl = total_market_value - total_cost
341
+ annual_coupons = sum(b["nominal"] * b["coupon"] / 100 for b in bonds) if bonds else Decimal("0")
342
+
343
+ # Emerging bonds specific
344
+ emerging_nominal = sum(b["nominal"] for b in emerging_bonds) if emerging_bonds else Decimal("0")
345
+ emerging_coupons = sum(b["nominal"] * b["coupon"] / 100 for b in emerging_bonds) if emerging_bonds else Decimal("0")
346
+ emerging_pnl = sum(b["nominal"] * (b["current_price"] - b["purchase_price"]) / 100 for b in emerging_bonds) if emerging_bonds else Decimal("0")
347
+
348
+ # Portfolio KPIs
349
+ col1, col2, col3, col4 = st.columns(4)
350
+ with col1:
351
+ st.metric("Valeur Nominale", format_amount(total_nominal))
352
+ with col2:
353
+ st.metric("Valeur de Marche", format_amount(total_market_value))
354
+ with col3:
355
+ pnl_delta = f"{'+' if pnl >= 0 else ''}{float(pnl/total_cost)*100:.2f}%" if total_cost > 0 else "0%"
356
+ st.metric("P&L Latent", format_amount(pnl), pnl_delta)
357
+ with col4:
358
+ st.metric("Coupons Annuels", format_amount(annual_coupons))
359
+
360
+ st.divider()
361
+
362
+ # ==================== SECTION EMERGENTS ====================
363
+ if emerging_bonds:
364
+ st.markdown("### Obligations Emergentes (Haut Rendement)")
365
+ st.markdown("""
366
+ <div style="background: linear-gradient(135deg, #4a1a1a, #1a1a2e); border: 1px solid #ff6b6b; border-radius: 12px; padding: 15px; margin-bottom: 20px;">
367
+ <div style="color: #ff6b6b; font-size: 12px; text-transform: uppercase;">Ukraine (War Bonds), Pologne, Roumanie</div>
368
+ </div>
369
+ """, unsafe_allow_html=True)
370
+
371
+ col1, col2, col3, col4 = st.columns(4)
372
+ with col1:
373
+ emerging_pct = float(emerging_nominal / total_nominal * 100) if total_nominal > 0 else 0
374
+ st.metric("Exposition Emergents", format_amount(emerging_nominal), f"{emerging_pct:.1f}% du portefeuille")
375
+ with col2:
376
+ st.metric("Coupons Emergents", format_amount(emerging_coupons), "annuels")
377
+ with col3:
378
+ emerging_pnl_pct = f"{'+' if emerging_pnl >= 0 else ''}{float(emerging_pnl / (emerging_nominal * Decimal('0.5')) * 100):.1f}%" if emerging_nominal > 0 else "0%"
379
+ st.metric("P&L Emergents", format_amount(emerging_pnl), emerging_pnl_pct)
380
+ with col4:
381
+ avg_yield = sum(b["coupon"] for b in emerging_bonds) / len(emerging_bonds) if emerging_bonds else Decimal("0")
382
+ st.metric("Rendement Moyen", f"{avg_yield:.2f}%", "brut")
383
+
384
+ # Emerging bonds details
385
+ for bond in emerging_bonds:
386
+ market_val = bond["nominal"] * bond["current_price"] / 100
387
+ cost_val = bond["nominal"] * bond["purchase_price"] / 100
388
+ bond_pnl = market_val - cost_val
389
+ pnl_pct = float(bond_pnl / cost_val * 100) if cost_val > 0 else 0
390
+
391
+ # Risk color and label based on country
392
+ if bond["country"] == "UA":
393
+ risk_color = "#ff6b6b"
394
+ risk_label = "WAR BOND"
395
+ elif bond["country"] == "RO":
396
+ risk_color = "#ffd93d"
397
+ risk_label = "HIGH YIELD"
398
+ else:
399
+ risk_color = "#00d4ff"
400
+ risk_label = "INVESTMENT GRADE"
401
+
402
+ st.markdown(f"""
403
+ <div style="background: #1e1e2f; border-left: 4px solid {risk_color}; padding: 12px; border-radius: 0 8px 8px 0; margin-bottom: 8px;">
404
+ <div style="display: flex; justify-content: space-between; align-items: center;">
405
+ <div>
406
+ <span style="color: white; font-weight: bold;">{bond['country']} | {bond['name']}</span>
407
+ <span style="background: {risk_color}; color: black; font-size: 10px; padding: 2px 6px; border-radius: 4px; margin-left: 10px;">{risk_label}</span>
408
+ </div>
409
+ <div style="text-align: right;">
410
+ <div style="color: #00d4ff;">{format_amount(bond['nominal'])} nominal</div>
411
+ <div style="color: {'#00ff88' if bond_pnl >= 0 else '#ff6b6b'};">P&L: {format_amount(bond_pnl)} ({pnl_pct:+.1f}%)</div>
412
+ </div>
413
+ </div>
414
+ <div style="color: #888; font-size: 12px; margin-top: 8px;">
415
+ Coupon: {bond['coupon']}% | Prix achat: {bond['purchase_price']}% | Prix actuel: {bond['current_price']}% | Echeance: {bond['maturity'].strftime('%d/%m/%Y')}
416
+ </div>
417
+ </div>
418
+ """, unsafe_allow_html=True)
419
+
420
+ st.divider()
421
+
422
+ # ==================== ALIMENTATION COMPTE TITRES (en premier) ====================
423
+ st.markdown("### Alimentation Compte Titres")
424
+ st.caption(f"Solde compte principal: {format_amount(data['accounts']['principal']['balance'])} | Solde compte titres: {format_amount(data['accounts']['titres']['balance'])}")
425
+
426
+ col1, col2, col3 = st.columns([2, 1, 1])
427
+ with col1:
428
+ transfer_amount = st.number_input(
429
+ "Montant a transferer (EUR)",
430
+ min_value=100000.0,
431
+ max_value=float(data["accounts"]["principal"]["balance"]),
432
+ step=100000.0,
433
+ value=1000000.0,
434
+ key="fund_titres"
435
+ )
436
+ with col2:
437
+ if st.button("Alimenter Compte Titres", type="primary", key="btn_fund"):
438
+ amount = Decimal(str(transfer_amount))
439
+ data["accounts"]["principal"]["balance"] -= amount
440
+ data["accounts"]["titres"]["balance"] += amount
441
+ data.get("transactions", []).append({
442
+ "date": datetime.now(),
443
+ "type": "internal_transfer",
444
+ "from": "principal",
445
+ "to": "titres",
446
+ "amount": amount,
447
+ "motif": "Alimentation compte titres",
448
+ })
449
+ st.success(f"Transfert de {format_amount(amount)} effectue")
450
+ st.rerun()
451
+ with col3:
452
+ if st.button("Retirer vers Principal", key="btn_withdraw"):
453
+ if data["accounts"]["titres"]["balance"] >= Decimal(str(transfer_amount)):
454
+ amount = Decimal(str(transfer_amount))
455
+ data["accounts"]["titres"]["balance"] -= amount
456
+ data["accounts"]["principal"]["balance"] += amount
457
+ st.success(f"Retrait de {format_amount(amount)} effectue")
458
+ st.rerun()
459
+ else:
460
+ st.error("Solde insuffisant")
461
+
462
+ st.divider()
463
+
464
+ # ==================== FORMULAIRE D'ACHAT ====================
465
+ st.markdown("### Acheter des Obligations")
466
+
467
+ # Filter by region
468
+ region = st.radio("Region", ["Toutes", "Zone Euro", "Hors Zone Euro"], horizontal=True, key="region_filter")
469
+
470
+ eurozone = ["FR", "DE", "IT", "ES", "NL", "BE", "AT", "PT", "IE", "FI"]
471
+ filtered = available
472
+ if region == "Zone Euro":
473
+ filtered = [b for b in available if b["country"] in eurozone]
474
+ elif region == "Hors Zone Euro":
475
+ filtered = [b for b in available if b["country"] not in eurozone]
476
+
477
+ # Bond selection
478
+ bond_options = {f"{b['country']} - {b['name']} (Rdt: {b['yield']}%)": b['isin'] for b in filtered}
479
+
480
+ if bond_options:
481
+ selected_label = st.selectbox("Selectionnez une obligation", list(bond_options.keys()), key="select_bond")
482
+ selected_isin = bond_options[selected_label]
483
+ selected_bond = next((b for b in filtered if b["isin"] == selected_isin), None)
484
+
485
+ if selected_bond:
486
+ col1, col2, col3 = st.columns(3)
487
+ with col1:
488
+ st.info(f"**{selected_bond['name']}**\nISIN: {selected_bond['isin']}")
489
+ with col2:
490
+ st.info(f"**Coupon:** {selected_bond['coupon']}%\n**Echeance:** {selected_bond['maturity'].strftime('%d/%m/%Y')}")
491
+ with col3:
492
+ st.info(f"**Prix:** {selected_bond['current_price']}%\n**Rendement:** {selected_bond['yield']}%")
493
+
494
+ col1, col2 = st.columns(2)
495
+ with col1:
496
+ buy_nominal = st.number_input(
497
+ "Nominal a acheter (EUR)",
498
+ min_value=100000.0,
499
+ max_value=10000000.0,
500
+ step=100000.0,
501
+ value=1000000.0,
502
+ key="buy_nominal_input"
503
+ )
504
+ buy_cost = Decimal(str(buy_nominal)) * selected_bond["current_price"] / 100
505
+ st.caption(f"Cout total: **{format_amount(buy_cost)}**")
506
+
507
+ with col2:
508
+ cash_available = data["accounts"]["titres"]["balance"]
509
+ st.caption(f"Cash disponible: {format_amount(cash_available)}")
510
+
511
+ if st.button("Acheter", type="primary", key="btn_buy_bond", disabled=buy_cost > cash_available):
512
+ if buy_cost > cash_available:
513
+ st.error("Fonds insuffisants! Alimentez d'abord le compte titres.")
514
+ else:
515
+ new_bond = {
516
+ "isin": selected_bond["isin"],
517
+ "name": selected_bond["name"],
518
+ "country": selected_bond["country"],
519
+ "country_name": selected_bond["country_name"],
520
+ "coupon": selected_bond["coupon"],
521
+ "maturity": selected_bond["maturity"],
522
+ "nominal": Decimal(str(buy_nominal)),
523
+ "purchase_price": selected_bond["current_price"],
524
+ "current_price": selected_bond["current_price"],
525
+ "purchase_date": date.today(),
526
+ "quantity": int(buy_nominal / 100000),
527
+ }
528
+ data["sovereign_bonds"].append(new_bond)
529
+ data["accounts"]["titres"]["balance"] -= buy_cost
530
+ if "bond_transactions" not in data:
531
+ data["bond_transactions"] = []
532
+ data["bond_transactions"].append({
533
+ "date": datetime.now(),
534
+ "type": "buy",
535
+ "isin": selected_bond["isin"],
536
+ "name": selected_bond["name"],
537
+ "nominal": Decimal(str(buy_nominal)),
538
+ "price": selected_bond["current_price"],
539
+ "amount": buy_cost,
540
+ })
541
+ st.success(f"Achat de {format_amount(Decimal(str(buy_nominal)))} nominal execute!")
542
+ st.rerun()
543
+
544
+ if buy_cost > cash_available:
545
+ st.warning(f"Il manque {format_amount(buy_cost - cash_available)} sur le compte titres")
546
+
547
+ st.divider()
548
+
549
+ # ==================== PORTEFEUILLE ACTUEL ====================
550
+ st.markdown("### Repartition Geographique")
551
+
552
+ if bonds:
553
+ countries = {}
554
+ for bond in bonds:
555
+ country = bond["country_name"]
556
+ if country not in countries:
557
+ countries[country] = Decimal("0")
558
+ countries[country] += bond["nominal"]
559
+
560
+ for country, nominal in sorted(countries.items(), key=lambda x: x[1], reverse=True):
561
+ pct = float(nominal / total_nominal * 100) if total_nominal > 0 else 0
562
+ col1, col2 = st.columns([1, 3])
563
+ with col1:
564
+ flag_map = {"France": "FR", "Allemagne": "DE", "Italie": "IT", "Espagne": "ES", "Etats-Unis": "US", "Royaume-Uni": "GB", "Japon": "JP", "Suisse": "CH", "Pays-Bas": "NL", "Belgique": "BE"}
565
+ flag = flag_map.get(country, "EU")
566
+ st.markdown(f"**{flag} {country}**")
567
+ st.caption(format_amount(nominal))
568
+ with col2:
569
+ st.progress(pct / 100)
570
+ st.caption(f"{pct:.1f}%")
571
+ else:
572
+ st.info("Aucune obligation en portefeuille")
573
+
574
+ st.divider()
575
+
576
+ # ==================== POSITIONS ACTUELLES ====================
577
+ st.markdown("### Positions Actuelles")
578
+
579
+ if bonds:
580
+ for idx, bond in enumerate(bonds):
581
+ market_val = bond["nominal"] * bond["current_price"] / 100
582
+ cost_val = bond["nominal"] * bond["purchase_price"] / 100
583
+ bond_pnl = market_val - cost_val
584
+ pnl_pct = float(bond_pnl / cost_val * 100) if cost_val > 0 else 0
585
+
586
+ with st.expander(f"{bond['country']} | {bond['name']} | {format_amount(bond['nominal'])} nominal", expanded=False):
587
+ col1, col2, col3, col4 = st.columns(4)
588
+ with col1:
589
+ st.markdown(f"**ISIN:** {bond['isin']}")
590
+ st.markdown(f"**Nominal:** {format_amount(bond['nominal'])}")
591
+ with col2:
592
+ st.markdown(f"**Coupon:** {bond['coupon']}%")
593
+ st.markdown(f"**Echeance:** {bond['maturity'].strftime('%d/%m/%Y')}")
594
+ with col3:
595
+ st.markdown(f"**Prix achat:** {bond['purchase_price']}%")
596
+ st.markdown(f"**Prix actuel:** {bond['current_price']}%")
597
+ with col4:
598
+ pnl_color = "green" if bond_pnl >= 0 else "red"
599
+ st.markdown(f"**Valeur marche:** {format_amount(market_val)}")
600
+ st.markdown(f"**P&L:** :{pnl_color}[{format_amount(bond_pnl)} ({pnl_pct:+.2f}%)]")
601
+
602
+ # Vente
603
+ st.markdown("---")
604
+ col1, col2, col3 = st.columns([2, 1, 1])
605
+ with col1:
606
+ sell_pct = st.slider(f"% a vendre", 10, 100, 100, 10, key=f"sell_pct_{idx}")
607
+ sell_nominal = bond["nominal"] * sell_pct / 100
608
+ sell_value = sell_nominal * bond["current_price"] / 100
609
+ st.caption(f"Vente: {format_amount(sell_nominal)} nominal = {format_amount(sell_value)}")
610
+ with col2:
611
+ if st.button("Vendre", key=f"sell_btn_{idx}", type="secondary"):
612
+ if sell_pct == 100:
613
+ data["sovereign_bonds"].remove(bond)
614
+ else:
615
+ bond["nominal"] -= sell_nominal
616
+ data["accounts"]["titres"]["balance"] += sell_value
617
+ if "bond_transactions" not in data:
618
+ data["bond_transactions"] = []
619
+ data["bond_transactions"].append({
620
+ "date": datetime.now(),
621
+ "type": "sell",
622
+ "isin": bond["isin"],
623
+ "name": bond["name"],
624
+ "nominal": sell_nominal,
625
+ "price": bond["current_price"],
626
+ "amount": sell_value,
627
+ })
628
+ st.success(f"Vente executee: {format_amount(sell_value)}")
629
+ st.rerun()
630
+ with col3:
631
+ txs = [t for t in data.get("bond_transactions", []) if t.get("isin") == bond["isin"]]
632
+ if txs:
633
+ st.caption(f"{len(txs)} transaction(s)")
634
+ else:
635
+ st.info("Aucune position. Achetez des obligations ci-dessus.")
636
+
637
+
638
+ def render_assurances_tab(data):
639
+ """Group insurance management"""
640
+ st.markdown("### Assurances Groupe")
641
+
642
+ insurances = data.get("insurances", [])
643
+ claims = data.get("claims", [])
644
+
645
+ # Calculate totals
646
+ total_coverage = sum(ins["coverage"] for ins in insurances) if insurances else Decimal("0")
647
+ total_premiums = sum(ins["premium"] for ins in insurances) if insurances else Decimal("0")
648
+ active_policies = len([ins for ins in insurances if ins["status"] == "active"])
649
+
650
+ # KPIs
651
+ col1, col2, col3, col4 = st.columns(4)
652
+ with col1:
653
+ st.metric("Couverture Totale", format_amount(total_coverage))
654
+ with col2:
655
+ st.metric("Primes Annuelles", format_amount(total_premiums))
656
+ with col3:
657
+ st.metric("Polices Actives", str(active_policies))
658
+ with col4:
659
+ pending_claims = len([c for c in claims if c["status"] == "pending"])
660
+ st.metric("Sinistres en cours", str(pending_claims))
661
+
662
+ st.divider()
663
+
664
+ # Insurance by type
665
+ st.markdown("### Polices d'Assurance")
666
+
667
+ type_labels = {
668
+ "D&O": ("RC Dirigeants", "#7b2cbf"),
669
+ "RC_PRO": ("RC Professionnelle", "#00d4ff"),
670
+ "CYBER": ("Cyber", "#ff6b6b"),
671
+ "PROPERTY": ("Immobilier", "#00ff88"),
672
+ "KEY_MAN": ("Homme-Cle", "#ffd93d"),
673
+ "CDS": ("Credit Default Swap", "#ff6b6b"),
674
+ }
675
+
676
+ for ins in insurances:
677
+ type_info = type_labels.get(ins["type"], (ins["type"], "#888"))
678
+ type_label, type_color = type_info
679
+
680
+ # Check if expiring soon (within 90 days)
681
+ days_to_expiry = (ins["end_date"] - date.today()).days
682
+ expiry_warning = days_to_expiry <= 90
683
+
684
+ st.markdown(f"""
685
+ <div style="background: #1e1e2f; border-left: 4px solid {type_color}; padding: 15px; border-radius: 0 8px 8px 0; margin-bottom: 12px;">
686
+ <div style="display: flex; justify-content: space-between; align-items: start;">
687
+ <div>
688
+ <span style="background: {type_color}; color: black; font-size: 10px; padding: 2px 8px; border-radius: 4px;">{type_label}</span>
689
+ <div style="color: white; font-weight: bold; font-size: 16px; margin-top: 8px;">{ins['name']}</div>
690
+ <div style="color: #888; font-size: 12px; margin-top: 4px;">Assureur: {ins['insurer']} | Police: {ins['policy_number']}</div>
691
+ </div>
692
+ <div style="text-align: right;">
693
+ <div style="color: #00d4ff; font-size: 18px; font-weight: bold;">{format_amount(ins['coverage'])}</div>
694
+ <div style="color: #888; font-size: 12px;">couverture</div>
695
+ <div style="color: {'#ff6b6b' if expiry_warning else '#00ff88'}; font-size: 12px; margin-top: 4px;">
696
+ {'Expire dans ' + str(days_to_expiry) + 'j' if expiry_warning else 'Valide'}
697
+ </div>
698
+ </div>
699
+ </div>
700
+ <div style="display: flex; gap: 30px; margin-top: 12px; color: #aaa; font-size: 13px;">
701
+ <div><span style="color: #888;">Prime:</span> {format_amount(ins['premium'])}/an</div>
702
+ <div><span style="color: #888;">Debut:</span> {ins['start_date'].strftime('%d/%m/%Y')}</div>
703
+ <div><span style="color: #888;">Fin:</span> {ins['end_date'].strftime('%d/%m/%Y')}</div>
704
+ </div>
705
+ </div>
706
+ """, unsafe_allow_html=True)
707
+
708
+ st.divider()
709
+
710
+ # CDS Section (special for sovereign risk)
711
+ cds_policies = [ins for ins in insurances if ins["type"] == "CDS"]
712
+ if cds_policies:
713
+ st.markdown("### Couverture Risque Souverain (CDS)")
714
+ st.markdown("""
715
+ <div style="background: linear-gradient(135deg, #4a1a1a, #1a1a2e); border: 1px solid #ff6b6b; border-radius: 12px; padding: 15px; margin-bottom: 20px;">
716
+ <div style="color: #ff6b6b; font-size: 12px; text-transform: uppercase;">Credit Default Swaps - Protection contre defaut souverain</div>
717
+ </div>
718
+ """, unsafe_allow_html=True)
719
+
720
+ for cds in cds_policies:
721
+ spread = cds.get("spread_bps", 0)
722
+ st.markdown(f"""
723
+ <div style="background: #1e1e2f; padding: 15px; border-radius: 8px; margin-bottom: 10px;">
724
+ <div style="display: flex; justify-content: space-between;">
725
+ <div>
726
+ <div style="color: white; font-weight: bold;">{cds['name']}</div>
727
+ <div style="color: #888; font-size: 12px;">Contrepartie: {cds['insurer']}</div>
728
+ </div>
729
+ <div style="text-align: right;">
730
+ <div style="color: #ff6b6b; font-size: 18px; font-weight: bold;">{spread} bps</div>
731
+ <div style="color: #888; font-size: 12px;">spread annuel</div>
732
+ </div>
733
+ </div>
734
+ <div style="margin-top: 10px; display: flex; gap: 20px; color: #aaa; font-size: 13px;">
735
+ <div>Notionnel: {format_amount(cds['coverage'])}</div>
736
+ <div>Prime annuelle: {format_amount(cds['premium'])}</div>
737
+ <div>Maturite: {cds['end_date'].strftime('%d/%m/%Y')}</div>
738
+ </div>
739
+ </div>
740
+ """, unsafe_allow_html=True)
741
+
742
+ st.divider()
743
+
744
+ # Claims section
745
+ st.markdown("### Historique des Sinistres")
746
+
747
+ if claims:
748
+ for claim in claims:
749
+ # Find insurance
750
+ ins = next((i for i in insurances if i["id"] == claim["insurance_id"]), None)
751
+ ins_name = ins["name"] if ins else "N/A"
752
+
753
+ status_colors = {"paid": "#00ff88", "pending": "#ffd93d", "rejected": "#ff6b6b"}
754
+ status_labels = {"paid": "Rembourse", "pending": "En cours", "rejected": "Rejete"}
755
+
756
+ col1, col2, col3, col4, col5 = st.columns([2, 2, 2, 2, 1])
757
+ with col1:
758
+ st.markdown(f"**{claim['type']}**")
759
+ st.caption(f"{claim['date'].strftime('%d/%m/%Y')}")
760
+ with col2:
761
+ st.caption(ins_name)
762
+ with col3:
763
+ st.markdown(f"Demande: {format_amount(claim['amount_claimed'])}")
764
+ with col4:
765
+ if claim["amount_paid"] > 0:
766
+ st.markdown(f":green[Paye: {format_amount(claim['amount_paid'])}]")
767
+ else:
768
+ st.caption("En attente")
769
+ with col5:
770
+ color = status_colors.get(claim["status"], "#888")
771
+ label = status_labels.get(claim["status"], claim["status"])
772
+ st.markdown(f"<span style='background: {color}; color: black; padding: 2px 8px; border-radius: 4px; font-size: 11px;'>{label}</span>", unsafe_allow_html=True)
773
+ st.divider()
774
+ else:
775
+ st.info("Aucun sinistre enregistre")
776
+
777
+ # Summary
778
+ st.markdown("### Synthese Annuelle")
779
+ col1, col2, col3 = st.columns(3)
780
+ with col1:
781
+ claims_paid = sum(c["amount_paid"] for c in claims if c["status"] == "paid")
782
+ st.metric("Sinistres Rembourses", format_amount(claims_paid))
783
+ with col2:
784
+ ratio = float(claims_paid / total_premiums * 100) if total_premiums > 0 else 0
785
+ st.metric("Ratio S/P", f"{ratio:.1f}%")
786
+ with col3:
787
+ st.metric("Cout Net Assurance", format_amount(total_premiums - claims_paid))
788
+
789
+
790
+ def render_consolidation_tab(data):
791
+ """Consolidated financials"""
792
+ st.markdown("### Bilan Consolide")
793
+
794
+ total_assets = sum(s["assets"] for s in data["subsidiaries"])
795
+ treasury = data["accounts"]["principal"]["balance"] + data["accounts"]["tresorerie"]["balance"]
796
+
797
+ col1, col2, col3, col4 = st.columns(4)
798
+ with col1:
799
+ st.metric("Total Actifs", f"{(total_assets + treasury)/1000000000:.2f}B EUR", "+5.2%")
800
+ with col2:
801
+ st.metric("Fonds Propres", "420M EUR", "+3.1%")
802
+ with col3:
803
+ st.metric("PNB Groupe", "285M EUR", "+8.4%")
804
+ with col4:
805
+ st.metric("Resultat Net", "62M EUR", "+12.3%")
806
+
807
+ st.divider()
808
+
809
+ st.markdown("### Repartition par Activite")
810
+
811
+ activities = [
812
+ {"name": "Banque de detail", "revenue": 180, "percent": 63},
813
+ {"name": "Gestion d'actifs", "revenue": 52, "percent": 18},
814
+ {"name": "Assurance", "revenue": 38, "percent": 13},
815
+ {"name": "Autres", "revenue": 15, "percent": 6},
816
+ ]
817
+
818
+ for act in activities:
819
+ col1, col2 = st.columns([1, 3])
820
+ with col1:
821
+ st.markdown(f"**{act['name']}**")
822
+ st.caption(f"{act['revenue']}M EUR")
823
+ with col2:
824
+ st.progress(act['percent'] / 100)
825
+
826
+ st.divider()
827
+
828
+ st.markdown("### Contribution des Filiales")
829
+
830
+ for sub in data["subsidiaries"]:
831
+ contribution = float(sub["assets"] / total_assets) * 100
832
+ st.markdown(f"**{sub['name']}** - {contribution:.1f}%")
833
+ st.progress(contribution / 100)
834
+
835
+
836
+ def render_risques_tab(data):
837
+ """Risk management"""
838
+ st.markdown("### Ratios Reglementaires")
839
+
840
+ col1, col2, col3 = st.columns(3)
841
+ with col1:
842
+ st.metric("Ratio CET1", "14.2%", "+0.3%")
843
+ st.caption("Minimum requis: 10.5%")
844
+ st.progress(0.142 / 0.20)
845
+ with col2:
846
+ st.metric("Ratio LCR", "142%", "+5%")
847
+ st.caption("Minimum requis: 100%")
848
+ st.progress(1.42 / 2.0)
849
+ with col3:
850
+ st.metric("Ratio NSFR", "118%", "+2%")
851
+ st.caption("Minimum requis: 100%")
852
+ st.progress(1.18 / 2.0)
853
+
854
+ st.divider()
855
+
856
+ st.markdown("### Exposition par Type de Risque")
857
+
858
+ risks = [
859
+ {"type": "Risque de credit", "exposure": "2.1B EUR", "provision": "45M EUR", "level": "medium"},
860
+ {"type": "Risque de marche", "exposure": "320M EUR", "provision": "8M EUR", "level": "low"},
861
+ {"type": "Risque operationnel", "exposure": "-", "provision": "12M EUR", "level": "low"},
862
+ {"type": "Risque de liquidite", "exposure": "450M EUR", "provision": "-", "level": "low"},
863
+ ]
864
+
865
+ for risk in risks:
866
+ col1, col2, col3, col4 = st.columns([2, 2, 2, 1])
867
+ with col1:
868
+ st.markdown(f"**{risk['type']}**")
869
+ with col2:
870
+ st.caption(f"Exposition: {risk['exposure']}")
871
+ with col3:
872
+ st.caption(f"Provision: {risk['provision']}")
873
+ with col4:
874
+ if risk['level'] == 'high':
875
+ st.error("Eleve")
876
+ elif risk['level'] == 'medium':
877
+ st.warning("Moyen")
878
+ else:
879
+ st.success("Faible")
880
+
881
+ st.divider()
882
+
883
+ st.markdown("### Concentration")
884
+
885
+ pool_total = sum(abs(s["pool_balance"]) for s in data["subsidiaries"])
886
+ for sub in data["subsidiaries"]:
887
+ concentration = float(abs(sub["pool_balance"]) / pool_total * 100) if pool_total > 0 else 0.0
888
+ st.markdown(f"{sub['name']}: {concentration:.1f}%")
889
+ st.progress(concentration / 100)
890
+
891
+
892
+ def render_gouvernance_tab(data):
893
+ """Governance"""
894
+ st.markdown("### Conseil d'Administration")
895
+
896
+ board = [
897
+ {"name": "Laurent Dubois", "role": "President-Directeur General", "since": "2018"},
898
+ {"name": "Marie-Claire Fontaine", "role": "Directrice Generale Deleguee", "since": "2019"},
899
+ {"name": "Philippe Martin", "role": "Directeur Financier", "since": "2020"},
900
+ {"name": "Isabelle Leroy", "role": "Directrice des Risques", "since": "2021"},
901
+ {"name": "Jean-Pierre Moreau", "role": "Administrateur Independant", "since": "2017"},
902
+ ]
903
+
904
+ for member in board:
905
+ col1, col2, col3 = st.columns([2, 2, 1])
906
+ with col1:
907
+ st.markdown(f"**{member['name']}**")
908
+ with col2:
909
+ st.caption(member['role'])
910
+ with col3:
911
+ st.caption(f"Depuis {member['since']}")
912
+
913
+ st.divider()
914
+
915
+ st.markdown("### Comites")
916
+
917
+ committees = [
918
+ {"name": "Comite d'Audit", "members": 4, "meetings": 12},
919
+ {"name": "Comite des Risques", "members": 3, "meetings": 24},
920
+ {"name": "Comite des Remunerations", "members": 3, "meetings": 4},
921
+ {"name": "Comite de Nomination", "members": 2, "meetings": 2},
922
+ ]
923
+
924
+ for committee in committees:
925
+ col1, col2, col3 = st.columns([2, 1, 1])
926
+ with col1:
927
+ st.markdown(f"**{committee['name']}**")
928
+ with col2:
929
+ st.caption(f"{committee['members']} membres")
930
+ with col3:
931
+ st.caption(f"{committee['meetings']} reunions/an")
932
+
933
+ st.divider()
934
+
935
+ st.markdown("### Documents Reglementaires")
936
+
937
+ col1, col2 = st.columns(2)
938
+ with col1:
939
+ st.download_button("Rapport Annuel 2025", "Contenu du rapport annuel...", "rapport_annuel_2025.pdf", mime="application/pdf")
940
+ st.download_button("Rapport Pilier 3", "Contenu du rapport Pilier 3...", "pilier3_2025.pdf", mime="application/pdf")
941
+ with col2:
942
+ st.download_button("Rapport RSE", "Contenu du rapport RSE...", "rse_2025.pdf", mime="application/pdf")
943
+ st.download_button("Charte Ethique", "Contenu de la charte...", "charte_ethique.pdf", mime="application/pdf")