Docfile commited on
Commit
73d203f
·
verified ·
1 Parent(s): 994d93e

Upload 67 files

Browse files
app/__pycache__/__init__.cpython-314.pyc CHANGED
Binary files a/app/__pycache__/__init__.cpython-314.pyc and b/app/__pycache__/__init__.cpython-314.pyc differ
 
app/models/__init__.py CHANGED
@@ -1,8 +1,11 @@
1
- from app import db, login_manager
2
- from flask_login import UserMixin
3
- from datetime import datetime, date, timezone
4
  import secrets
5
  import string
 
 
 
 
 
 
6
 
7
 
8
  @login_manager.user_loader
@@ -12,13 +15,18 @@ def load_user(user_id):
12
 
13
  class AppSettings(db.Model):
14
  """Model for storing application settings that can be modified from admin panel"""
 
15
  __tablename__ = "app_settings"
16
 
17
  id = db.Column(db.Integer, primary_key=True)
18
  key = db.Column(db.String(100), unique=True, nullable=False)
19
  value = db.Column(db.Text, nullable=True)
20
  description = db.Column(db.String(255), nullable=True)
21
- updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
 
 
 
 
22
 
23
  @staticmethod
24
  def get_setting(key, default=None):
@@ -43,12 +51,37 @@ class AppSettings(db.Model):
43
  @staticmethod
44
  def get_app_name():
45
  """Get the application name"""
46
- return AppSettings.get_setting('app_name', 'Apex Ores')
47
 
48
  @staticmethod
49
  def get_app_logo():
50
  """Get the application logo URL"""
51
- return AppSettings.get_setting('app_logo', None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
 
54
  class User(UserMixin, db.Model):
@@ -66,10 +99,16 @@ class User(UserMixin, db.Model):
66
  bonus_balance = db.Column(db.Float, default=0.0)
67
  total_gains = db.Column(db.Float, default=0.0)
68
 
 
 
 
69
  # Registration bonus tracking - bonus is locked until first subscription
70
  registration_bonus = db.Column(db.Float, default=0.0)
71
  registration_bonus_unlocked = db.Column(db.Boolean, default=False)
72
 
 
 
 
73
  created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
74
  last_login = db.Column(db.DateTime, nullable=True)
75
  last_daily_bonus = db.Column(db.Date, nullable=True)
@@ -110,7 +149,9 @@ class User(UserMixin, db.Model):
110
  @property
111
  def display_balance(self):
112
  """Total balance displayed to user (includes locked bonus)"""
113
- return self.balance + (self.registration_bonus if not self.registration_bonus_unlocked else 0)
 
 
114
 
115
  @property
116
  def withdrawable_balance(self):
@@ -132,6 +173,20 @@ class User(UserMixin, db.Model):
132
  return True
133
  return False
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  class Metal(db.Model):
137
  __tablename__ = "metals"
@@ -182,9 +237,120 @@ class Transaction(db.Model):
182
  description = db.Column(db.String(255))
183
  status = db.Column(db.String(20), default="pending")
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
186
  processed_at = db.Column(db.DateTime, nullable=True)
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  class ReferralCommission(db.Model):
190
  __tablename__ = "referral_commissions"
@@ -194,10 +360,15 @@ class ReferralCommission(db.Model):
194
  referred_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
195
 
196
  level = db.Column(db.Integer, nullable=False)
 
 
 
197
  commission_percentage = db.Column(db.Float, nullable=False)
198
  commission_amount = db.Column(db.Float, nullable=False)
199
 
200
- purchase_amount = db.Column(db.Float, nullable=False)
 
 
201
  created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
202
 
203
  referrer = db.relationship(
@@ -207,6 +378,86 @@ class ReferralCommission(db.Model):
207
  "User", foreign_keys=[referred_user_id], backref="commissions_generated"
208
  )
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
  class Notification(db.Model):
212
  __tablename__ = "notifications"
 
 
 
 
1
  import secrets
2
  import string
3
+ from datetime import date, datetime, timedelta, timezone
4
+
5
+ from flask import current_app
6
+ from flask_login import UserMixin
7
+
8
+ from app import db, login_manager
9
 
10
 
11
  @login_manager.user_loader
 
15
 
16
  class AppSettings(db.Model):
17
  """Model for storing application settings that can be modified from admin panel"""
18
+
19
  __tablename__ = "app_settings"
20
 
21
  id = db.Column(db.Integer, primary_key=True)
22
  key = db.Column(db.String(100), unique=True, nullable=False)
23
  value = db.Column(db.Text, nullable=True)
24
  description = db.Column(db.String(255), nullable=True)
25
+ updated_at = db.Column(
26
+ db.DateTime,
27
+ default=lambda: datetime.now(timezone.utc),
28
+ onupdate=lambda: datetime.now(timezone.utc),
29
+ )
30
 
31
  @staticmethod
32
  def get_setting(key, default=None):
 
51
  @staticmethod
52
  def get_app_name():
53
  """Get the application name"""
54
+ return AppSettings.get_setting("app_name", "Apex Ores")
55
 
56
  @staticmethod
57
  def get_app_logo():
58
  """Get the application logo URL"""
59
+ return AppSettings.get_setting("app_logo", None)
60
+
61
+ @staticmethod
62
+ def get_fake_user_count():
63
+ """Get the fake user count added by admin for display purposes"""
64
+ value = AppSettings.get_setting("fake_user_count", "0")
65
+ try:
66
+ return int(value)
67
+ except (ValueError, TypeError):
68
+ return 0
69
+
70
+ @staticmethod
71
+ def set_fake_user_count(count):
72
+ """Set the fake user count"""
73
+ AppSettings.set_setting(
74
+ "fake_user_count",
75
+ str(count),
76
+ "Nombre d'utilisateurs fictifs ajoutés pour l'affichage",
77
+ )
78
+
79
+ @staticmethod
80
+ def get_total_displayed_users():
81
+ """Get total users to display (real + fake)"""
82
+ real_users = User.query.count()
83
+ fake_users = AppSettings.get_fake_user_count()
84
+ return real_users + fake_users
85
 
86
 
87
  class User(UserMixin, db.Model):
 
99
  bonus_balance = db.Column(db.Float, default=0.0)
100
  total_gains = db.Column(db.Float, default=0.0)
101
 
102
+ # Referral earnings tracking
103
+ referral_earnings = db.Column(db.Float, default=0.0) # Total earned from referrals
104
+
105
  # Registration bonus tracking - bonus is locked until first subscription
106
  registration_bonus = db.Column(db.Float, default=0.0)
107
  registration_bonus_unlocked = db.Column(db.Boolean, default=False)
108
 
109
+ # Track if user has seen the welcome popup
110
+ has_seen_welcome_popup = db.Column(db.Boolean, default=False)
111
+
112
  created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
113
  last_login = db.Column(db.DateTime, nullable=True)
114
  last_daily_bonus = db.Column(db.Date, nullable=True)
 
149
  @property
150
  def display_balance(self):
151
  """Total balance displayed to user (includes locked bonus)"""
152
+ return self.balance + (
153
+ self.registration_bonus if not self.registration_bonus_unlocked else 0
154
+ )
155
 
156
  @property
157
  def withdrawable_balance(self):
 
173
  return True
174
  return False
175
 
176
+ def get_referrer(self):
177
+ """Get the user who referred this user"""
178
+ if self.referred_by_code:
179
+ return User.query.filter_by(referral_code=self.referred_by_code).first()
180
+ return None
181
+
182
+ def get_referrals(self):
183
+ """Get all users referred by this user"""
184
+ return User.query.filter_by(referred_by_code=self.referral_code).all()
185
+
186
+ def get_referral_count(self):
187
+ """Get count of users referred by this user"""
188
+ return User.query.filter_by(referred_by_code=self.referral_code).count()
189
+
190
 
191
  class Metal(db.Model):
192
  __tablename__ = "metals"
 
237
  description = db.Column(db.String(255))
238
  status = db.Column(db.String(20), default="pending")
239
 
240
+ # Withdrawal specific fields
241
+ gross_amount = db.Column(db.Float, nullable=True) # Amount before fees
242
+ fee_amount = db.Column(db.Float, nullable=True) # Fee amount
243
+ net_amount = db.Column(
244
+ db.Float, nullable=True
245
+ ) # Amount after fees (what user receives)
246
+
247
+ # 24h delay processing
248
+ scheduled_process_time = db.Column(
249
+ db.DateTime, nullable=True
250
+ ) # When the withdrawal can be processed
251
+ admin_action = db.Column(
252
+ db.String(20), nullable=True
253
+ ) # 'approved', 'rejected', or None (auto-approved after 24h)
254
+ admin_action_time = db.Column(db.DateTime, nullable=True) # When admin took action
255
+
256
  created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
257
  processed_at = db.Column(db.DateTime, nullable=True)
258
 
259
+ @staticmethod
260
+ def get_withdrawal_fee_percentage():
261
+ """Get withdrawal fee percentage from config"""
262
+ try:
263
+ return current_app.config.get("WITHDRAWAL_FEE_PERCENTAGE", 0.15)
264
+ except RuntimeError:
265
+ # Outside application context, use default
266
+ return 0.15
267
+
268
+ @staticmethod
269
+ def get_withdrawal_delay_hours():
270
+ """Get withdrawal delay in hours from config"""
271
+ try:
272
+ return current_app.config.get("WITHDRAWAL_DELAY_HOURS", 24)
273
+ except RuntimeError:
274
+ return 24
275
+
276
+ @staticmethod
277
+ def calculate_withdrawal_fee(amount):
278
+ """Calculate withdrawal fee based on config percentage"""
279
+ fee_percentage = Transaction.get_withdrawal_fee_percentage()
280
+ fee = amount * fee_percentage
281
+ net = amount - fee
282
+ return {
283
+ "gross": amount,
284
+ "fee": fee,
285
+ "net": net,
286
+ "fee_percentage": fee_percentage * 100,
287
+ }
288
+
289
+ @staticmethod
290
+ def calculate_scheduled_process_time(from_time=None):
291
+ """
292
+ Calculate when a withdrawal should be processed (configurable delay, business days only)
293
+ If withdrawal is on Friday, Saturday, or Sunday, schedule for Monday + delay
294
+ """
295
+ if from_time is None:
296
+ from_time = datetime.now(timezone.utc)
297
+
298
+ delay_hours = Transaction.get_withdrawal_delay_hours()
299
+
300
+ # Add delay hours
301
+ process_time = from_time + timedelta(hours=delay_hours)
302
+
303
+ # Check if it falls on a weekend (Saturday=5, Sunday=6)
304
+ while process_time.weekday() in [5, 6]:
305
+ # Move to next day
306
+ process_time += timedelta(days=1)
307
+
308
+ return process_time
309
+
310
+ def can_be_auto_processed(self):
311
+ """Check if withdrawal can be auto-processed (delay passed and no admin action)"""
312
+ if self.type != "withdrawal" or self.status != "pending":
313
+ return False
314
+ if self.admin_action is not None:
315
+ return False
316
+ if self.scheduled_process_time is None:
317
+ return False
318
+
319
+ now = datetime.now(timezone.utc)
320
+ # Check if current time is a business day
321
+ if now.weekday() in [5, 6]:
322
+ return False
323
+
324
+ return now >= self.scheduled_process_time
325
+
326
+ @staticmethod
327
+ def get_total_withdrawal_fees():
328
+ """Get total fees collected from all completed withdrawals"""
329
+ total = (
330
+ db.session.query(db.func.sum(Transaction.fee_amount))
331
+ .filter(
332
+ Transaction.type == "withdrawal",
333
+ Transaction.status.in_(["approved", "completed"]),
334
+ Transaction.fee_amount.isnot(None),
335
+ )
336
+ .scalar()
337
+ )
338
+ return total or 0
339
+
340
+ @staticmethod
341
+ def get_total_withdrawals_amount():
342
+ """Get total amount of all completed withdrawals (gross amount)"""
343
+ total = (
344
+ db.session.query(db.func.sum(Transaction.gross_amount))
345
+ .filter(
346
+ Transaction.type == "withdrawal",
347
+ Transaction.status.in_(["approved", "completed"]),
348
+ Transaction.gross_amount.isnot(None),
349
+ )
350
+ .scalar()
351
+ )
352
+ return total or 0
353
+
354
 
355
  class ReferralCommission(db.Model):
356
  __tablename__ = "referral_commissions"
 
360
  referred_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
361
 
362
  level = db.Column(db.Integer, nullable=False)
363
+ commission_type = db.Column(
364
+ db.String(20), nullable=False
365
+ ) # 'purchase' or 'daily_gain'
366
  commission_percentage = db.Column(db.Float, nullable=False)
367
  commission_amount = db.Column(db.Float, nullable=False)
368
 
369
+ purchase_amount = db.Column(db.Float, nullable=True) # For purchase commissions
370
+ gain_amount = db.Column(db.Float, nullable=True) # For daily gain commissions
371
+
372
  created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
373
 
374
  referrer = db.relationship(
 
378
  "User", foreign_keys=[referred_user_id], backref="commissions_generated"
379
  )
380
 
381
+ @staticmethod
382
+ def get_purchase_commission_rate():
383
+ """Get purchase commission rate from config (default 15%)"""
384
+ try:
385
+ return current_app.config.get("REFERRAL_PURCHASE_COMMISSION", 0.15)
386
+ except RuntimeError:
387
+ # Outside application context, use default
388
+ return 0.15
389
+
390
+ @staticmethod
391
+ def get_daily_gain_commission_rate():
392
+ """Get daily gain commission rate from config (default 3%)"""
393
+ try:
394
+ return current_app.config.get("REFERRAL_DAILY_GAIN_COMMISSION", 0.03)
395
+ except RuntimeError:
396
+ # Outside application context, use default
397
+ return 0.03
398
+
399
+ @staticmethod
400
+ def calculate_purchase_commission(purchase_amount):
401
+ """Calculate commission on plan purchase using config rate"""
402
+ rate = ReferralCommission.get_purchase_commission_rate()
403
+ return purchase_amount * rate
404
+
405
+ @staticmethod
406
+ def calculate_daily_gain_commission(gain_amount):
407
+ """Calculate commission on daily gains using config rate"""
408
+ rate = ReferralCommission.get_daily_gain_commission_rate()
409
+ return gain_amount * rate
410
+
411
+ @staticmethod
412
+ def create_purchase_commission(referrer, referred_user, purchase_amount):
413
+ """Create a commission entry for a plan purchase"""
414
+ rate = ReferralCommission.get_purchase_commission_rate()
415
+ commission_amount = purchase_amount * rate
416
+
417
+ commission = ReferralCommission(
418
+ referrer_id=referrer.id,
419
+ referred_user_id=referred_user.id,
420
+ level=1,
421
+ commission_type="purchase",
422
+ commission_percentage=rate * 100,
423
+ commission_amount=commission_amount,
424
+ purchase_amount=purchase_amount,
425
+ )
426
+
427
+ # Add commission to referrer's balance
428
+ referrer.balance += commission_amount
429
+ referrer.referral_earnings = (
430
+ referrer.referral_earnings or 0
431
+ ) + commission_amount
432
+
433
+ db.session.add(commission)
434
+ return commission
435
+
436
+ @staticmethod
437
+ def create_daily_gain_commission(referrer, referred_user, gain_amount):
438
+ """Create a commission entry for daily gains"""
439
+ rate = ReferralCommission.get_daily_gain_commission_rate()
440
+ commission_amount = gain_amount * rate
441
+
442
+ commission = ReferralCommission(
443
+ referrer_id=referrer.id,
444
+ referred_user_id=referred_user.id,
445
+ level=1,
446
+ commission_type="daily_gain",
447
+ commission_percentage=rate * 100,
448
+ commission_amount=commission_amount,
449
+ gain_amount=gain_amount,
450
+ )
451
+
452
+ # Add commission to referrer's balance
453
+ referrer.balance += commission_amount
454
+ referrer.referral_earnings = (
455
+ referrer.referral_earnings or 0
456
+ ) + commission_amount
457
+
458
+ db.session.add(commission)
459
+ return commission
460
+
461
 
462
  class Notification(db.Model):
463
  __tablename__ = "notifications"
app/models/__pycache__/__init__.cpython-314.pyc CHANGED
Binary files a/app/models/__pycache__/__init__.cpython-314.pyc and b/app/models/__pycache__/__init__.cpython-314.pyc differ
 
app/routes/__pycache__/admin_routes.cpython-314.pyc CHANGED
Binary files a/app/routes/__pycache__/admin_routes.cpython-314.pyc and b/app/routes/__pycache__/admin_routes.cpython-314.pyc differ
 
app/routes/__pycache__/main.cpython-314.pyc CHANGED
Binary files a/app/routes/__pycache__/main.cpython-314.pyc and b/app/routes/__pycache__/main.cpython-314.pyc differ
 
app/routes/__pycache__/payments.cpython-314.pyc CHANGED
Binary files a/app/routes/__pycache__/payments.cpython-314.pyc and b/app/routes/__pycache__/payments.cpython-314.pyc differ
 
app/routes/admin_routes.py CHANGED
@@ -1,17 +1,27 @@
1
- from flask import Blueprint, render_template, redirect, url_for, flash, request, current_app
2
- from werkzeug.utils import secure_filename
3
  from datetime import datetime, timezone
 
 
 
 
 
 
 
 
 
 
 
 
4
  from app import db
5
  from app.models import (
6
- User,
7
  Metal,
8
- UserMetal,
9
- Transaction,
10
- ReferralCommission,
11
  Notification,
12
- AppSettings,
 
 
 
13
  )
14
- import os
15
 
16
  bp = Blueprint("admin_panel", __name__, url_prefix="/admin")
17
 
@@ -37,20 +47,33 @@ def inject_admin_context():
37
  @bp.route("/")
38
  def dashboard():
39
  """Admin dashboard with overview statistics"""
 
40
  total_users = User.query.count()
41
  active_users = User.query.filter_by(account_active=True).count()
 
 
 
 
42
  total_transactions = Transaction.query.count()
43
  pending_withdrawals = Transaction.query.filter_by(
44
  type="withdrawal", status="pending"
45
  ).count()
46
 
 
47
  total_deposits = (
48
  db.session.query(db.func.sum(Transaction.amount))
49
- .filter(Transaction.type == "deposit", Transaction.status == "completed")
50
  .scalar()
51
  or 0
52
  )
53
 
 
 
 
 
 
 
 
54
  total_investments = (
55
  db.session.query(db.func.sum(UserMetal.purchase_price))
56
  .filter(UserMetal.is_active == True)
@@ -58,6 +81,13 @@ def dashboard():
58
  or 0
59
  )
60
 
 
 
 
 
 
 
 
61
  recent_transactions = (
62
  Transaction.query.order_by(Transaction.created_at.desc()).limit(10).all()
63
  )
@@ -66,10 +96,15 @@ def dashboard():
66
  "admin/dashboard.html",
67
  total_users=total_users,
68
  active_users=active_users,
 
 
69
  total_transactions=total_transactions,
70
  pending_withdrawals=pending_withdrawals,
71
  total_deposits=total_deposits,
 
 
72
  total_investments=total_investments,
 
73
  recent_transactions=recent_transactions,
74
  )
75
 
@@ -117,12 +152,21 @@ def user_detail(user_id):
117
  # Get referrals
118
  referrals = User.query.filter_by(referred_by_code=user.referral_code).all()
119
 
 
 
 
 
 
 
 
 
120
  return render_template(
121
  "admin/user_detail.html",
122
  user=user,
123
  transactions=transactions,
124
  investments=investments,
125
  referrals=referrals,
 
126
  )
127
 
128
 
@@ -200,7 +244,7 @@ def withdrawals():
200
  type="withdrawal", status="rejected"
201
  ).count()
202
 
203
- # Total pending amount
204
  total_pending_amount = (
205
  db.session.query(db.func.sum(Transaction.amount))
206
  .filter(Transaction.type == "withdrawal", Transaction.status == "pending")
@@ -208,6 +252,21 @@ def withdrawals():
208
  or 0
209
  )
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  return render_template(
212
  "admin/withdrawals.html",
213
  withdrawals=withdrawals,
@@ -215,6 +274,8 @@ def withdrawals():
215
  approved_count=approved_count,
216
  rejected_count=rejected_count,
217
  total_pending_amount=total_pending_amount,
 
 
218
  )
219
 
220
 
@@ -232,28 +293,44 @@ def process_withdrawal(transaction_id):
232
  if action == "approve":
233
  transaction.status = "approved"
234
  transaction.processed_at = datetime.now(timezone.utc)
 
 
 
 
 
 
235
 
236
  notification = Notification(
237
  user_id=transaction.user_id,
238
  title="Retrait Approuvé",
239
- message=f"Votre demande de retrait de {transaction.amount} FCFA a été approuvée et sera traitée prochainement.",
 
 
 
 
240
  type="withdrawal",
241
  )
242
  db.session.add(notification)
243
 
244
- flash("Retrait approuvé.", "success")
 
 
 
245
 
246
  elif action == "reject":
247
  transaction.status = "rejected"
248
  transaction.processed_at = datetime.now(timezone.utc)
 
 
249
 
 
250
  user = User.query.get(transaction.user_id)
251
  user.balance += transaction.amount
252
 
253
  notification = Notification(
254
  user_id=user.id,
255
  title="Retrait Rejeté",
256
- message=f"Votre demande de retrait de {transaction.amount} FCFA a été rejetée. Le montant a été remboursé sur votre solde.",
257
  type="withdrawal",
258
  )
259
  db.session.add(notification)
@@ -264,6 +341,57 @@ def process_withdrawal(transaction_id):
264
  return redirect(url_for("admin_panel.withdrawals"))
265
 
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  @bp.route("/transactions")
268
  def transactions():
269
  """List all transactions"""
@@ -370,13 +498,15 @@ def delete_metal(metal_id):
370
  metal = Metal.query.get_or_404(metal_id)
371
 
372
  # Check if there are active subscriptions
373
- active_subscriptions = UserMetal.query.filter_by(metal_id=metal_id, is_active=True).count()
 
 
374
 
375
  if active_subscriptions > 0:
376
  flash(
377
  f"Impossible de supprimer '{metal.name}' : {active_subscriptions} souscription(s) active(s). "
378
  f"Désactivez d'abord le plan et attendez que toutes les souscriptions expirent.",
379
- "error"
380
  )
381
  return redirect(url_for("admin_panel.metals"))
382
 
@@ -402,6 +532,11 @@ def settings():
402
  app_name = request.form.get("app_name", "").strip()
403
  app_logo = request.form.get("app_logo", "").strip()
404
 
 
 
 
 
 
405
  # Handle file upload
406
  if "logo_file" in request.files:
407
  file = request.files["logo_file"]
@@ -409,9 +544,7 @@ def settings():
409
  # Save the uploaded file
410
  filename = secure_filename(file.filename)
411
  # Create uploads directory if it doesn't exist
412
- upload_folder = os.path.join(
413
- current_app.root_path, "static", "uploads"
414
- )
415
  os.makedirs(upload_folder, exist_ok=True)
416
 
417
  # Save file with timestamp to avoid conflicts
@@ -430,7 +563,9 @@ def settings():
430
  AppSettings.set_setting("app_name", app_name, "Nom de l'application")
431
 
432
  if app_logo:
433
- AppSettings.set_setting("app_logo", app_logo, "URL du logo de l'application")
 
 
434
  elif not request.form.get("remove_logo") and not app_logo:
435
  # Keep existing logo if no new one provided
436
  pass
@@ -441,11 +576,17 @@ def settings():
441
  # Get current settings
442
  current_app_name = AppSettings.get_app_name()
443
  current_app_logo = AppSettings.get_app_logo()
 
 
 
444
 
445
  return render_template(
446
  "admin/settings.html",
447
  current_app_name=current_app_name,
448
  current_app_logo=current_app_logo,
 
 
 
449
  )
450
 
451
 
@@ -467,10 +608,49 @@ def unlock_bonus(user_id):
467
  db.session.add(notification)
468
  db.session.commit()
469
 
470
- flash(
471
- f"Bonus de {bonus_amount} FCFA débloqué pour {user.phone}.", "success"
472
- )
473
  else:
474
  flash("Cet utilisateur n'a pas de bonus bloqué.", "warning")
475
 
476
  return redirect(url_for("admin_panel.users"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
 
2
  from datetime import datetime, timezone
3
+
4
+ from flask import (
5
+ Blueprint,
6
+ current_app,
7
+ flash,
8
+ redirect,
9
+ render_template,
10
+ request,
11
+ url_for,
12
+ )
13
+ from werkzeug.utils import secure_filename
14
+
15
  from app import db
16
  from app.models import (
17
+ AppSettings,
18
  Metal,
 
 
 
19
  Notification,
20
+ ReferralCommission,
21
+ Transaction,
22
+ User,
23
+ UserMetal,
24
  )
 
25
 
26
  bp = Blueprint("admin_panel", __name__, url_prefix="/admin")
27
 
 
47
  @bp.route("/")
48
  def dashboard():
49
  """Admin dashboard with overview statistics"""
50
+ # User statistics
51
  total_users = User.query.count()
52
  active_users = User.query.filter_by(account_active=True).count()
53
+ fake_user_count = AppSettings.get_fake_user_count()
54
+ displayed_user_count = AppSettings.get_total_displayed_users()
55
+
56
+ # Transaction counts
57
  total_transactions = Transaction.query.count()
58
  pending_withdrawals = Transaction.query.filter_by(
59
  type="withdrawal", status="pending"
60
  ).count()
61
 
62
+ # Total deposits (completed purchases via Lygos or manual)
63
  total_deposits = (
64
  db.session.query(db.func.sum(Transaction.amount))
65
+ .filter(Transaction.type == "purchase", Transaction.status == "completed")
66
  .scalar()
67
  or 0
68
  )
69
 
70
+ # Total withdrawals (approved/completed)
71
+ total_withdrawals = Transaction.get_total_withdrawals_amount()
72
+
73
+ # Total withdrawal fees collected (15%)
74
+ total_withdrawal_fees = Transaction.get_total_withdrawal_fees()
75
+
76
+ # Active investments value
77
  total_investments = (
78
  db.session.query(db.func.sum(UserMetal.purchase_price))
79
  .filter(UserMetal.is_active == True)
 
81
  or 0
82
  )
83
 
84
+ # Total referral commissions paid
85
+ total_referral_commissions = (
86
+ db.session.query(db.func.sum(ReferralCommission.commission_amount)).scalar()
87
+ or 0
88
+ )
89
+
90
+ # Recent transactions
91
  recent_transactions = (
92
  Transaction.query.order_by(Transaction.created_at.desc()).limit(10).all()
93
  )
 
96
  "admin/dashboard.html",
97
  total_users=total_users,
98
  active_users=active_users,
99
+ fake_user_count=fake_user_count,
100
+ displayed_user_count=displayed_user_count,
101
  total_transactions=total_transactions,
102
  pending_withdrawals=pending_withdrawals,
103
  total_deposits=total_deposits,
104
+ total_withdrawals=total_withdrawals,
105
+ total_withdrawal_fees=total_withdrawal_fees,
106
  total_investments=total_investments,
107
+ total_referral_commissions=total_referral_commissions,
108
  recent_transactions=recent_transactions,
109
  )
110
 
 
152
  # Get referrals
153
  referrals = User.query.filter_by(referred_by_code=user.referral_code).all()
154
 
155
+ # Get referral commissions earned
156
+ referral_commissions = (
157
+ ReferralCommission.query.filter_by(referrer_id=user.id)
158
+ .order_by(ReferralCommission.created_at.desc())
159
+ .limit(20)
160
+ .all()
161
+ )
162
+
163
  return render_template(
164
  "admin/user_detail.html",
165
  user=user,
166
  transactions=transactions,
167
  investments=investments,
168
  referrals=referrals,
169
+ referral_commissions=referral_commissions,
170
  )
171
 
172
 
 
244
  type="withdrawal", status="rejected"
245
  ).count()
246
 
247
+ # Total pending amount (gross)
248
  total_pending_amount = (
249
  db.session.query(db.func.sum(Transaction.amount))
250
  .filter(Transaction.type == "withdrawal", Transaction.status == "pending")
 
252
  or 0
253
  )
254
 
255
+ # Total pending fees
256
+ total_pending_fees = (
257
+ db.session.query(db.func.sum(Transaction.fee_amount))
258
+ .filter(
259
+ Transaction.type == "withdrawal",
260
+ Transaction.status == "pending",
261
+ Transaction.fee_amount.isnot(None),
262
+ )
263
+ .scalar()
264
+ or 0
265
+ )
266
+
267
+ # Total withdrawal fees collected
268
+ total_fees_collected = Transaction.get_total_withdrawal_fees()
269
+
270
  return render_template(
271
  "admin/withdrawals.html",
272
  withdrawals=withdrawals,
 
274
  approved_count=approved_count,
275
  rejected_count=rejected_count,
276
  total_pending_amount=total_pending_amount,
277
+ total_pending_fees=total_pending_fees,
278
+ total_fees_collected=total_fees_collected,
279
  )
280
 
281
 
 
293
  if action == "approve":
294
  transaction.status = "approved"
295
  transaction.processed_at = datetime.now(timezone.utc)
296
+ transaction.admin_action = "approved"
297
+ transaction.admin_action_time = datetime.now(timezone.utc)
298
+
299
+ # Net amount is what user receives after 15% fee
300
+ net_amount = transaction.net_amount or transaction.amount
301
+ fee_amount = transaction.fee_amount or 0
302
 
303
  notification = Notification(
304
  user_id=transaction.user_id,
305
  title="Retrait Approuvé",
306
+ message=(
307
+ f"Votre demande de retrait de {transaction.amount:.0f} FCFA a été approuvée.\n"
308
+ f"Frais (15%): {fee_amount:.0f} FCFA\n"
309
+ f"Montant envoyé: {net_amount:.0f} FCFA"
310
+ ),
311
  type="withdrawal",
312
  )
313
  db.session.add(notification)
314
 
315
+ flash(
316
+ f"Retrait approuvé. Montant net à envoyer: {net_amount:.0f} FCFA (après {fee_amount:.0f} FCFA de frais).",
317
+ "success",
318
+ )
319
 
320
  elif action == "reject":
321
  transaction.status = "rejected"
322
  transaction.processed_at = datetime.now(timezone.utc)
323
+ transaction.admin_action = "rejected"
324
+ transaction.admin_action_time = datetime.now(timezone.utc)
325
 
326
+ # Refund the gross amount to user
327
  user = User.query.get(transaction.user_id)
328
  user.balance += transaction.amount
329
 
330
  notification = Notification(
331
  user_id=user.id,
332
  title="Retrait Rejeté",
333
+ message=f"Votre demande de retrait de {transaction.amount:.0f} FCFA a été rejetée. Le montant a été remboursé sur votre solde.",
334
  type="withdrawal",
335
  )
336
  db.session.add(notification)
 
341
  return redirect(url_for("admin_panel.withdrawals"))
342
 
343
 
344
+ @bp.route("/auto_process_withdrawals")
345
+ def auto_process_withdrawals():
346
+ """Auto-process withdrawals that have passed their 24h delay without admin action"""
347
+ now = datetime.now(timezone.utc)
348
+
349
+ # Skip weekends
350
+ if now.weekday() in [5, 6]:
351
+ flash("Les retraits automatiques ne sont pas traités les week-ends.", "warning")
352
+ return redirect(url_for("admin_panel.withdrawals"))
353
+
354
+ # Find pending withdrawals that can be auto-processed
355
+ pending_withdrawals = Transaction.query.filter(
356
+ Transaction.type == "withdrawal",
357
+ Transaction.status == "pending",
358
+ Transaction.admin_action.is_(None),
359
+ Transaction.scheduled_process_time <= now,
360
+ ).all()
361
+
362
+ processed_count = 0
363
+ for transaction in pending_withdrawals:
364
+ transaction.status = "approved"
365
+ transaction.processed_at = now
366
+ transaction.admin_action = "auto_approved"
367
+ transaction.admin_action_time = now
368
+
369
+ net_amount = transaction.net_amount or transaction.amount
370
+ fee_amount = transaction.fee_amount or 0
371
+
372
+ notification = Notification(
373
+ user_id=transaction.user_id,
374
+ title="Retrait Traité Automatiquement",
375
+ message=(
376
+ f"Votre retrait de {transaction.amount:.0f} FCFA a été traité automatiquement.\n"
377
+ f"Frais (15%): {fee_amount:.0f} FCFA\n"
378
+ f"Montant envoyé: {net_amount:.0f} FCFA"
379
+ ),
380
+ type="withdrawal",
381
+ )
382
+ db.session.add(notification)
383
+ processed_count += 1
384
+
385
+ db.session.commit()
386
+
387
+ if processed_count > 0:
388
+ flash(f"{processed_count} retrait(s) traité(s) automatiquement.", "success")
389
+ else:
390
+ flash("Aucun retrait à traiter automatiquement.", "info")
391
+
392
+ return redirect(url_for("admin_panel.withdrawals"))
393
+
394
+
395
  @bp.route("/transactions")
396
  def transactions():
397
  """List all transactions"""
 
498
  metal = Metal.query.get_or_404(metal_id)
499
 
500
  # Check if there are active subscriptions
501
+ active_subscriptions = UserMetal.query.filter_by(
502
+ metal_id=metal_id, is_active=True
503
+ ).count()
504
 
505
  if active_subscriptions > 0:
506
  flash(
507
  f"Impossible de supprimer '{metal.name}' : {active_subscriptions} souscription(s) active(s). "
508
  f"Désactivez d'abord le plan et attendez que toutes les souscriptions expirent.",
509
+ "error",
510
  )
511
  return redirect(url_for("admin_panel.metals"))
512
 
 
532
  app_name = request.form.get("app_name", "").strip()
533
  app_logo = request.form.get("app_logo", "").strip()
534
 
535
+ # Handle fake user count
536
+ fake_user_count = request.form.get("fake_user_count", type=int)
537
+ if fake_user_count is not None and fake_user_count >= 0:
538
+ AppSettings.set_fake_user_count(fake_user_count)
539
+
540
  # Handle file upload
541
  if "logo_file" in request.files:
542
  file = request.files["logo_file"]
 
544
  # Save the uploaded file
545
  filename = secure_filename(file.filename)
546
  # Create uploads directory if it doesn't exist
547
+ upload_folder = os.path.join(current_app.root_path, "static", "uploads")
 
 
548
  os.makedirs(upload_folder, exist_ok=True)
549
 
550
  # Save file with timestamp to avoid conflicts
 
563
  AppSettings.set_setting("app_name", app_name, "Nom de l'application")
564
 
565
  if app_logo:
566
+ AppSettings.set_setting(
567
+ "app_logo", app_logo, "URL du logo de l'application"
568
+ )
569
  elif not request.form.get("remove_logo") and not app_logo:
570
  # Keep existing logo if no new one provided
571
  pass
 
576
  # Get current settings
577
  current_app_name = AppSettings.get_app_name()
578
  current_app_logo = AppSettings.get_app_logo()
579
+ current_fake_user_count = AppSettings.get_fake_user_count()
580
+ real_user_count = User.query.count()
581
+ displayed_user_count = AppSettings.get_total_displayed_users()
582
 
583
  return render_template(
584
  "admin/settings.html",
585
  current_app_name=current_app_name,
586
  current_app_logo=current_app_logo,
587
+ current_fake_user_count=current_fake_user_count,
588
+ real_user_count=real_user_count,
589
+ displayed_user_count=displayed_user_count,
590
  )
591
 
592
 
 
608
  db.session.add(notification)
609
  db.session.commit()
610
 
611
+ flash(f"Bonus de {bonus_amount} FCFA débloqué pour {user.phone}.", "success")
 
 
612
  else:
613
  flash("Cet utilisateur n'a pas de bonus bloqué.", "warning")
614
 
615
  return redirect(url_for("admin_panel.users"))
616
+
617
+
618
+ @bp.route("/referral_commissions")
619
+ def referral_commissions():
620
+ """View all referral commissions"""
621
+ page = request.args.get("page", 1, type=int)
622
+ commission_type = request.args.get("type", "")
623
+
624
+ query = ReferralCommission.query
625
+
626
+ if commission_type:
627
+ query = query.filter_by(commission_type=commission_type)
628
+
629
+ commissions = query.order_by(ReferralCommission.created_at.desc()).paginate(
630
+ page=page, per_page=30, error_out=False
631
+ )
632
+
633
+ # Statistics
634
+ total_purchase_commissions = (
635
+ db.session.query(db.func.sum(ReferralCommission.commission_amount))
636
+ .filter(ReferralCommission.commission_type == "purchase")
637
+ .scalar()
638
+ or 0
639
+ )
640
+
641
+ total_daily_commissions = (
642
+ db.session.query(db.func.sum(ReferralCommission.commission_amount))
643
+ .filter(ReferralCommission.commission_type == "daily_gain")
644
+ .scalar()
645
+ or 0
646
+ )
647
+
648
+ total_commissions = total_purchase_commissions + total_daily_commissions
649
+
650
+ return render_template(
651
+ "admin/referral_commissions.html",
652
+ commissions=commissions,
653
+ total_purchase_commissions=total_purchase_commissions,
654
+ total_daily_commissions=total_daily_commissions,
655
+ total_commissions=total_commissions,
656
+ )
app/routes/main.py CHANGED
@@ -1,11 +1,28 @@
1
  from datetime import date, datetime, timedelta, timezone
2
 
3
- from flask import Blueprint, flash, redirect, render_template, request, url_for
 
 
 
 
 
 
 
 
 
4
  from flask_login import current_user, login_required
5
 
6
  from app import db
7
  from app.forms import DepositForm, WithdrawalForm
8
- from app.models import Metal, Notification, Transaction, User, UserMetal
 
 
 
 
 
 
 
 
9
  from config.config import config
10
 
11
  bp = Blueprint("main", __name__)
@@ -15,10 +32,35 @@ bp = Blueprint("main", __name__)
15
  def index():
16
  if current_user.is_authenticated:
17
  return redirect(url_for("main.dashboard"))
18
- return render_template("index.html")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
 
21
  @bp.route("/dashboard")
 
22
  def dashboard():
23
  user = current_user
24
 
@@ -38,6 +80,18 @@ def dashboard():
38
  user_id=user.id, is_read=False
39
  ).count()
40
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  return render_template(
42
  "dashboard.html",
43
  user=user,
@@ -45,9 +99,23 @@ def dashboard():
45
  total_daily_gains=total_daily_gains,
46
  notifications_count=notifications_count,
47
  today=date.today(),
 
 
 
 
 
48
  )
49
 
50
 
 
 
 
 
 
 
 
 
 
51
  @bp.route("/market")
52
  @login_required
53
  def market():
@@ -115,20 +183,99 @@ def buy_metal(metal_id):
115
  )
116
  db.session.add(notification)
117
 
 
 
 
118
  db.session.commit()
119
 
120
  flash(f"Achat de {metal.name} effectué avec succès !", "success")
121
  return redirect(url_for("main.dashboard"))
122
 
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  @bp.route("/referral")
125
  @login_required
126
  def referral():
127
  user = current_user
128
  referred_users = User.query.filter_by(referred_by_code=user.referral_code).all()
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  return render_template(
131
- "referral.html", referral_code=user.referral_code, referred_users=referred_users
 
 
 
 
 
 
 
132
  )
133
 
134
 
@@ -172,6 +319,14 @@ def withdraw():
172
  locked_bonus = current_user.locked_bonus
173
  has_subscription = current_user.has_active_subscription
174
 
 
 
 
 
 
 
 
 
175
  if form.validate_on_submit():
176
  # Check if user is trying to withdraw more than their withdrawable balance
177
  if form.amount.data > withdrawable_balance:
@@ -189,31 +344,56 @@ def withdraw():
189
  withdrawable_balance=withdrawable_balance,
190
  locked_bonus=locked_bonus,
191
  has_subscription=has_subscription,
 
192
  )
193
 
194
- # Deduct from main balance only
 
 
 
 
 
 
195
  current_user.balance -= form.amount.data
196
 
 
197
  transaction = Transaction(
198
  user_id=current_user.id,
199
  type="withdrawal",
200
- amount=form.amount.data,
 
 
 
201
  description=f"Retrait vers {form.country_code.data}{form.phone.data}",
202
  status="pending",
 
203
  )
204
  db.session.add(transaction)
205
 
 
 
 
206
  notification = Notification(
207
  user_id=current_user.id,
208
  title="Demande de Retrait",
209
- message=f"Votre demande de retrait de {form.amount.data} FCFA a été soumise.",
 
 
 
 
 
210
  type="withdrawal",
211
  )
212
  db.session.add(notification)
213
 
214
  db.session.commit()
215
 
216
- flash("Votre demande de retrait a été soumise avec succès.", "success")
 
 
 
 
 
217
  return redirect(url_for("main.dashboard"))
218
 
219
  return render_template(
@@ -222,6 +402,10 @@ def withdraw():
222
  withdrawable_balance=withdrawable_balance,
223
  locked_bonus=locked_bonus,
224
  has_subscription=has_subscription,
 
 
 
 
225
  )
226
 
227
 
 
1
  from datetime import date, datetime, timedelta, timezone
2
 
3
+ from flask import (
4
+ Blueprint,
5
+ current_app,
6
+ flash,
7
+ jsonify,
8
+ redirect,
9
+ render_template,
10
+ request,
11
+ url_for,
12
+ )
13
  from flask_login import current_user, login_required
14
 
15
  from app import db
16
  from app.forms import DepositForm, WithdrawalForm
17
+ from app.models import (
18
+ AppSettings,
19
+ Metal,
20
+ Notification,
21
+ ReferralCommission,
22
+ Transaction,
23
+ User,
24
+ UserMetal,
25
+ )
26
  from config.config import config
27
 
28
  bp = Blueprint("main", __name__)
 
32
  def index():
33
  if current_user.is_authenticated:
34
  return redirect(url_for("main.dashboard"))
35
+
36
+ # Get displayed user count (real + fake)
37
+ displayed_user_count = AppSettings.get_total_displayed_users()
38
+
39
+ # Get commission rates from config for display
40
+ purchase_commission_rate = current_app.config.get(
41
+ "REFERRAL_PURCHASE_COMMISSION", 0.15
42
+ )
43
+ daily_gain_commission_rate = current_app.config.get(
44
+ "REFERRAL_DAILY_GAIN_COMMISSION", 0.03
45
+ )
46
+
47
+ return render_template(
48
+ "index.html",
49
+ displayed_user_count=displayed_user_count,
50
+ purchase_commission_rate=int(purchase_commission_rate * 100),
51
+ daily_gain_commission_rate=int(daily_gain_commission_rate * 100),
52
+ )
53
+
54
+
55
+ @bp.route("/api/user-count")
56
+ def get_user_count():
57
+ """API endpoint to get the current user count for real-time display"""
58
+ displayed_user_count = AppSettings.get_total_displayed_users()
59
+ return jsonify({"count": displayed_user_count})
60
 
61
 
62
  @bp.route("/dashboard")
63
+ @login_required
64
  def dashboard():
65
  user = current_user
66
 
 
80
  user_id=user.id, is_read=False
81
  ).count()
82
 
83
+ # Check if user should see the welcome popup
84
+ show_welcome_popup = not user.has_seen_welcome_popup
85
+
86
+ # Get commission rates from config
87
+ purchase_commission_rate = current_app.config.get(
88
+ "REFERRAL_PURCHASE_COMMISSION", 0.15
89
+ )
90
+ daily_gain_commission_rate = current_app.config.get(
91
+ "REFERRAL_DAILY_GAIN_COMMISSION", 0.03
92
+ )
93
+ withdrawal_fee_rate = current_app.config.get("WITHDRAWAL_FEE_PERCENTAGE", 0.15)
94
+
95
  return render_template(
96
  "dashboard.html",
97
  user=user,
 
99
  total_daily_gains=total_daily_gains,
100
  notifications_count=notifications_count,
101
  today=date.today(),
102
+ show_welcome_popup=show_welcome_popup,
103
+ active_metals=active_metals,
104
+ purchase_commission_rate=int(purchase_commission_rate * 100),
105
+ daily_gain_commission_rate=int(daily_gain_commission_rate * 100),
106
+ withdrawal_fee_rate=int(withdrawal_fee_rate * 100),
107
  )
108
 
109
 
110
+ @bp.route("/dismiss-welcome-popup")
111
+ @login_required
112
+ def dismiss_welcome_popup():
113
+ """Mark the welcome popup as seen"""
114
+ current_user.has_seen_welcome_popup = True
115
+ db.session.commit()
116
+ return jsonify({"success": True})
117
+
118
+
119
  @bp.route("/market")
120
  @login_required
121
  def market():
 
183
  )
184
  db.session.add(notification)
185
 
186
+ # Process referral commission (15% on purchase)
187
+ process_purchase_referral_commission(user, metal.price)
188
+
189
  db.session.commit()
190
 
191
  flash(f"Achat de {metal.name} effectué avec succès !", "success")
192
  return redirect(url_for("main.dashboard"))
193
 
194
 
195
+ def process_purchase_referral_commission(user, purchase_amount):
196
+ """Process 15% referral commission on plan purchase"""
197
+ referrer = user.get_referrer()
198
+ if referrer:
199
+ commission = ReferralCommission.create_purchase_commission(
200
+ referrer, user, purchase_amount
201
+ )
202
+
203
+ # Create notification for referrer
204
+ notification = Notification(
205
+ user_id=referrer.id,
206
+ title="Commission de Parrainage !",
207
+ message=f"Vous avez reçu {commission.commission_amount:.0f} FCFA (15%) de commission sur l'achat de {user.name}.",
208
+ type="referral",
209
+ )
210
+ db.session.add(notification)
211
+
212
+
213
+ def process_daily_gain_referral_commission(user, gain_amount):
214
+ """Process 3% referral commission on daily gains"""
215
+ referrer = user.get_referrer()
216
+ if referrer:
217
+ commission = ReferralCommission.create_daily_gain_commission(
218
+ referrer, user, gain_amount
219
+ )
220
+
221
+ # Create notification for referrer (less verbose, only for significant amounts)
222
+ if commission.commission_amount >= 10:
223
+ notification = Notification(
224
+ user_id=referrer.id,
225
+ title="Commission sur Gains",
226
+ message=f"Vous avez reçu {commission.commission_amount:.0f} FCFA (3%) sur les gains de {user.name}.",
227
+ type="referral",
228
+ )
229
+ db.session.add(notification)
230
+
231
+
232
  @bp.route("/referral")
233
  @login_required
234
  def referral():
235
  user = current_user
236
  referred_users = User.query.filter_by(referred_by_code=user.referral_code).all()
237
 
238
+ # Calculate total referral earnings
239
+ total_referral_earnings = user.referral_earnings or 0
240
+
241
+ # Get referral statistics
242
+ total_purchase_commissions = (
243
+ db.session.query(db.func.sum(ReferralCommission.commission_amount))
244
+ .filter(
245
+ ReferralCommission.referrer_id == user.id,
246
+ ReferralCommission.commission_type == "purchase",
247
+ )
248
+ .scalar()
249
+ or 0
250
+ )
251
+
252
+ total_daily_commissions = (
253
+ db.session.query(db.func.sum(ReferralCommission.commission_amount))
254
+ .filter(
255
+ ReferralCommission.referrer_id == user.id,
256
+ ReferralCommission.commission_type == "daily_gain",
257
+ )
258
+ .scalar()
259
+ or 0
260
+ )
261
+
262
+ # Get commission rates from config
263
+ purchase_commission_rate = current_app.config.get(
264
+ "REFERRAL_PURCHASE_COMMISSION", 0.15
265
+ )
266
+ daily_gain_commission_rate = current_app.config.get(
267
+ "REFERRAL_DAILY_GAIN_COMMISSION", 0.03
268
+ )
269
+
270
  return render_template(
271
+ "referral.html",
272
+ referral_code=user.referral_code,
273
+ referred_users=referred_users,
274
+ total_referral_earnings=total_referral_earnings,
275
+ total_purchase_commissions=total_purchase_commissions,
276
+ total_daily_commissions=total_daily_commissions,
277
+ purchase_commission_rate=int(purchase_commission_rate * 100),
278
+ daily_gain_commission_rate=int(daily_gain_commission_rate * 100),
279
  )
280
 
281
 
 
319
  locked_bonus = current_user.locked_bonus
320
  has_subscription = current_user.has_active_subscription
321
 
322
+ # Get withdrawal config values
323
+ withdrawal_fee_rate = current_app.config.get("WITHDRAWAL_FEE_PERCENTAGE", 0.15)
324
+ withdrawal_delay_hours = current_app.config.get("WITHDRAWAL_DELAY_HOURS", 24)
325
+ withdrawal_min_amount = current_app.config.get("WITHDRAWAL_MIN_AMOUNT", 500)
326
+
327
+ # Calculate fee preview
328
+ fee_preview = Transaction.calculate_withdrawal_fee(withdrawable_balance)
329
+
330
  if form.validate_on_submit():
331
  # Check if user is trying to withdraw more than their withdrawable balance
332
  if form.amount.data > withdrawable_balance:
 
344
  withdrawable_balance=withdrawable_balance,
345
  locked_bonus=locked_bonus,
346
  has_subscription=has_subscription,
347
+ fee_preview=fee_preview,
348
  )
349
 
350
+ # Calculate withdrawal fees (15%)
351
+ fee_info = Transaction.calculate_withdrawal_fee(form.amount.data)
352
+
353
+ # Calculate scheduled process time (24h later, business days only)
354
+ scheduled_time = Transaction.calculate_scheduled_process_time()
355
+
356
+ # Deduct gross amount from user balance
357
  current_user.balance -= form.amount.data
358
 
359
+ # Create transaction with fee information
360
  transaction = Transaction(
361
  user_id=current_user.id,
362
  type="withdrawal",
363
+ amount=form.amount.data, # This is the gross amount requested
364
+ gross_amount=fee_info["gross"],
365
+ fee_amount=fee_info["fee"],
366
+ net_amount=fee_info["net"],
367
  description=f"Retrait vers {form.country_code.data}{form.phone.data}",
368
  status="pending",
369
+ scheduled_process_time=scheduled_time,
370
  )
371
  db.session.add(transaction)
372
 
373
+ # Format the scheduled time for display
374
+ scheduled_display = scheduled_time.strftime("%d/%m/%Y à %H:%M")
375
+
376
  notification = Notification(
377
  user_id=current_user.id,
378
  title="Demande de Retrait",
379
+ message=(
380
+ f"Votre demande de retrait de {form.amount.data} FCFA a été soumise.\n"
381
+ f"Frais (15%): {fee_info['fee']:.0f} FCFA\n"
382
+ f"Montant net: {fee_info['net']:.0f} FCFA\n"
383
+ f"Traitement prévu le: {scheduled_display}"
384
+ ),
385
  type="withdrawal",
386
  )
387
  db.session.add(notification)
388
 
389
  db.session.commit()
390
 
391
+ flash(
392
+ f"Votre demande de retrait de {form.amount.data} FCFA a été soumise. "
393
+ f"Après frais (15%), vous recevrez {fee_info['net']:.0f} FCFA. "
394
+ f"Traitement prévu le {scheduled_display}.",
395
+ "success",
396
+ )
397
  return redirect(url_for("main.dashboard"))
398
 
399
  return render_template(
 
402
  withdrawable_balance=withdrawable_balance,
403
  locked_bonus=locked_bonus,
404
  has_subscription=has_subscription,
405
+ fee_preview=fee_preview,
406
+ withdrawal_fee_rate=int(withdrawal_fee_rate * 100),
407
+ withdrawal_delay_hours=withdrawal_delay_hours,
408
+ withdrawal_min_amount=withdrawal_min_amount,
409
  )
410
 
411
 
app/routes/payments.py CHANGED
@@ -1,37 +1,70 @@
1
- from flask import Blueprint, render_template, redirect, url_for, flash, request, current_app, session
2
- from flask_login import login_required, current_user
3
- import requests
4
  import json
5
- import time
6
  import re
7
- from datetime import datetime, timezone, timedelta
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  from app import db
9
- from app.models import User, Metal, UserMetal, Transaction, Notification
 
 
 
 
 
 
 
 
 
10
 
11
- bp = Blueprint('payments', __name__)
12
 
13
- def create_lygos_payment_link(amount, shop_name, message, order_id, success_url=None, failure_url=None):
 
 
14
  payload = {
15
  "amount": int(amount),
16
  "shop_name": shop_name,
17
  "message": message,
18
  "order_id": order_id,
19
- "success_url": success_url or url_for('payments.success', _external=True, payment_provider='lygos', order_id=order_id),
20
- "failure_url": failure_url or url_for('payments.error', _external=True, payment_provider='lygos', order_id=order_id)
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
22
 
23
  headers = {
24
- "api-key": current_app.config.get('LYGOS_API_KEY'),
25
- "Content-Type": "application/json"
26
  }
27
 
28
  try:
29
  current_app.logger.info(f"Creating Lygos payment link for order {order_id}")
30
  response = requests.post(
31
- current_app.config.get('LYGOS_CREATE_GATEWAY_URL'),
32
  headers=headers,
33
  json=payload,
34
- timeout=30
35
  )
36
  response.raise_for_status()
37
  response_data = response.json()
@@ -40,7 +73,46 @@ def create_lygos_payment_link(amount, shop_name, message, order_id, success_url=
40
  current_app.logger.error(f"Lygos Error: {e}")
41
  return None
42
 
43
- @bp.route('/buy_plan_lygos/<int:plan_id>')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  @login_required
45
  def buy_plan_lygos(plan_id):
46
  plan = Metal.query.get_or_404(plan_id)
@@ -54,27 +126,33 @@ def buy_plan_lygos(plan_id):
54
 
55
  payment_link = create_lygos_payment_link(
56
  amount=plan.price,
57
- shop_name=current_app.config.get('LYGOS_SHOP_NAME', 'Apex Ores'),
58
  message=message,
59
- order_id=order_id
60
  )
61
 
62
  if payment_link:
63
  return redirect(payment_link)
64
  else:
65
- flash("Erreur lors de la création du lien de paiement. Veuillez réessayer.", "error")
66
- return redirect(url_for('main.market'))
 
 
 
67
 
68
- @bp.route('/success')
 
69
  def success():
70
- payment_provider = request.args.get('payment_provider')
71
- order_id = request.args.get('order_id')
72
 
73
- current_app.logger.info(f"Payment success callback: provider={payment_provider}, order_id={order_id}")
 
 
74
 
75
- if payment_provider == 'lygos' and order_id:
76
  # Verify payment status with Lygos API
77
- headers = {"api-key": current_app.config.get('LYGOS_API_KEY')}
78
  verify_url = f"{current_app.config.get('LYGOS_GET_PAYIN_STATUS_URL')}{order_id}"
79
 
80
  try:
@@ -84,8 +162,8 @@ def success():
84
 
85
  current_app.logger.info(f"Lygos verification response: {data}")
86
 
87
- if data.get('status') == 'COMPLETED':
88
- match = re.match(r'LYGOSP(\d+)U(\d+)T\d+', order_id)
89
  if match:
90
  plan_id = int(match.group(1))
91
  user_id = int(match.group(2))
@@ -94,65 +172,88 @@ def success():
94
  plan = Metal.query.get(plan_id)
95
 
96
  if user and plan:
97
- existing_tx = Transaction.query.filter_by(description=f"Lygos Order: {order_id}").first()
 
 
98
  if not existing_tx:
99
  # Check if this is the user's first subscription and unlock bonus
100
- if user.locked_bonus > 0 and not user.registration_bonus_unlocked:
 
 
 
101
  bonus_amount = user.registration_bonus
102
  user.unlock_registration_bonus()
103
 
104
  # Create notification for bonus unlock
105
  bonus_notification = Notification(
106
  user_id=user.id,
107
- title="Bonus Débloqué !",
108
  message=f"Félicitations ! Votre bonus d'inscription de {bonus_amount} FCFA a été débloqué et ajouté à votre solde.",
109
- type="bonus"
110
  )
111
  db.session.add(bonus_notification)
112
- current_app.logger.info(f"Registration bonus of {bonus_amount} FCFA unlocked for user {user.id}")
 
 
113
 
114
- expiry_date = datetime.now(timezone.utc) + timedelta(days=plan.cycle_days)
 
 
 
115
  user_metal = UserMetal(
116
  user_id=user.id,
117
  metal_id=plan.id,
118
  purchase_price=plan.price,
119
- expiry_date=expiry_date
120
  )
121
  db.session.add(user_metal)
122
 
 
123
  transaction = Transaction(
124
  user_id=user.id,
125
  type="purchase",
126
  amount=plan.price,
127
  description=f"Lygos Order: {order_id}",
128
- status="completed"
129
  )
130
  db.session.add(transaction)
131
 
 
132
  notification = Notification(
133
  user_id=user.id,
134
- title="Adoption Réussie",
135
- message=f"Votre adoption de {plan.name} via Lygos a été confirmée.",
136
- type="purchase"
137
  )
138
  db.session.add(notification)
 
 
 
 
139
  db.session.commit()
140
 
141
- flash(f"Félicitations ! Votre plan {plan.name} est maintenant actif.", "success")
142
- return redirect(url_for('main.dashboard'))
 
 
 
143
  else:
144
  flash("Cette commande a déjà été traitée.", "info")
145
- return redirect(url_for('main.dashboard'))
146
 
147
  else:
148
- flash(f"Le paiement n'a pas encore été complété (Statut: {data.get('status')}).", "warning")
 
 
 
149
  except Exception as e:
150
  current_app.logger.error(f"Verification Error: {e}")
151
  flash("Erreur lors de la vérification du paiement.", "error")
152
 
153
- return redirect(url_for('main.market'))
 
154
 
155
- @bp.route('/error')
156
  def error():
157
  flash("Le paiement a échoué ou a été annulé.", "error")
158
- return redirect(url_for('main.market'))
 
 
 
 
1
  import json
 
2
  import re
3
+ import time
4
+ from datetime import datetime, timedelta, timezone
5
+
6
+ import requests
7
+ from flask import (
8
+ Blueprint,
9
+ current_app,
10
+ flash,
11
+ redirect,
12
+ render_template,
13
+ request,
14
+ session,
15
+ url_for,
16
+ )
17
+ from flask_login import current_user, login_required
18
+
19
  from app import db
20
+ from app.models import (
21
+ Metal,
22
+ Notification,
23
+ ReferralCommission,
24
+ Transaction,
25
+ User,
26
+ UserMetal,
27
+ )
28
+
29
+ bp = Blueprint("payments", __name__)
30
 
 
31
 
32
+ def create_lygos_payment_link(
33
+ amount, shop_name, message, order_id, success_url=None, failure_url=None
34
+ ):
35
  payload = {
36
  "amount": int(amount),
37
  "shop_name": shop_name,
38
  "message": message,
39
  "order_id": order_id,
40
+ "success_url": success_url
41
+ or url_for(
42
+ "payments.success",
43
+ _external=True,
44
+ payment_provider="lygos",
45
+ order_id=order_id,
46
+ ),
47
+ "failure_url": failure_url
48
+ or url_for(
49
+ "payments.error",
50
+ _external=True,
51
+ payment_provider="lygos",
52
+ order_id=order_id,
53
+ ),
54
  }
55
 
56
  headers = {
57
+ "api-key": current_app.config.get("LYGOS_API_KEY"),
58
+ "Content-Type": "application/json",
59
  }
60
 
61
  try:
62
  current_app.logger.info(f"Creating Lygos payment link for order {order_id}")
63
  response = requests.post(
64
+ current_app.config.get("LYGOS_CREATE_GATEWAY_URL"),
65
  headers=headers,
66
  json=payload,
67
+ timeout=30,
68
  )
69
  response.raise_for_status()
70
  response_data = response.json()
 
73
  current_app.logger.error(f"Lygos Error: {e}")
74
  return None
75
 
76
+
77
+ def process_referral_commission(user, purchase_amount):
78
+ """
79
+ Process 15% referral commission on plan purchase.
80
+ The referrer (parrain) receives 15% of the purchase amount.
81
+ """
82
+ referrer = user.get_referrer()
83
+ if referrer:
84
+ try:
85
+ # Create the commission (15% of purchase)
86
+ commission = ReferralCommission.create_purchase_commission(
87
+ referrer, user, purchase_amount
88
+ )
89
+
90
+ # Create notification for referrer
91
+ notification = Notification(
92
+ user_id=referrer.id,
93
+ title="Commission de Parrainage ! 🎉",
94
+ message=(
95
+ f"Vous avez reçu {commission.commission_amount:.0f} FCFA (15%) "
96
+ f"de commission sur l'achat de plan de {user.name}. "
97
+ f"Merci de parrainer des amis !"
98
+ ),
99
+ type="referral",
100
+ )
101
+ db.session.add(notification)
102
+
103
+ current_app.logger.info(
104
+ f"Referral commission of {commission.commission_amount} FCFA "
105
+ f"credited to user {referrer.id} for referred user {user.id}"
106
+ )
107
+
108
+ return commission
109
+ except Exception as e:
110
+ current_app.logger.error(f"Error processing referral commission: {e}")
111
+ return None
112
+ return None
113
+
114
+
115
+ @bp.route("/buy_plan_lygos/<int:plan_id>")
116
  @login_required
117
  def buy_plan_lygos(plan_id):
118
  plan = Metal.query.get_or_404(plan_id)
 
126
 
127
  payment_link = create_lygos_payment_link(
128
  amount=plan.price,
129
+ shop_name=current_app.config.get("LYGOS_SHOP_NAME", "Apex Ores"),
130
  message=message,
131
+ order_id=order_id,
132
  )
133
 
134
  if payment_link:
135
  return redirect(payment_link)
136
  else:
137
+ flash(
138
+ "Erreur lors de la création du lien de paiement. Veuillez réessayer.",
139
+ "error",
140
+ )
141
+ return redirect(url_for("main.market"))
142
 
143
+
144
+ @bp.route("/success")
145
  def success():
146
+ payment_provider = request.args.get("payment_provider")
147
+ order_id = request.args.get("order_id")
148
 
149
+ current_app.logger.info(
150
+ f"Payment success callback: provider={payment_provider}, order_id={order_id}"
151
+ )
152
 
153
+ if payment_provider == "lygos" and order_id:
154
  # Verify payment status with Lygos API
155
+ headers = {"api-key": current_app.config.get("LYGOS_API_KEY")}
156
  verify_url = f"{current_app.config.get('LYGOS_GET_PAYIN_STATUS_URL')}{order_id}"
157
 
158
  try:
 
162
 
163
  current_app.logger.info(f"Lygos verification response: {data}")
164
 
165
+ if data.get("status") == "COMPLETED":
166
+ match = re.match(r"LYGOSP(\d+)U(\d+)T\d+", order_id)
167
  if match:
168
  plan_id = int(match.group(1))
169
  user_id = int(match.group(2))
 
172
  plan = Metal.query.get(plan_id)
173
 
174
  if user and plan:
175
+ existing_tx = Transaction.query.filter_by(
176
+ description=f"Lygos Order: {order_id}"
177
+ ).first()
178
  if not existing_tx:
179
  # Check if this is the user's first subscription and unlock bonus
180
+ if (
181
+ user.locked_bonus > 0
182
+ and not user.registration_bonus_unlocked
183
+ ):
184
  bonus_amount = user.registration_bonus
185
  user.unlock_registration_bonus()
186
 
187
  # Create notification for bonus unlock
188
  bonus_notification = Notification(
189
  user_id=user.id,
190
+ title="Bonus Débloqué ! 🎁",
191
  message=f"Félicitations ! Votre bonus d'inscription de {bonus_amount} FCFA a été débloqué et ajouté à votre solde.",
192
+ type="bonus",
193
  )
194
  db.session.add(bonus_notification)
195
+ current_app.logger.info(
196
+ f"Registration bonus of {bonus_amount} FCFA unlocked for user {user.id}"
197
+ )
198
 
199
+ # Create user metal subscription
200
+ expiry_date = datetime.now(timezone.utc) + timedelta(
201
+ days=plan.cycle_days
202
+ )
203
  user_metal = UserMetal(
204
  user_id=user.id,
205
  metal_id=plan.id,
206
  purchase_price=plan.price,
207
+ expiry_date=expiry_date,
208
  )
209
  db.session.add(user_metal)
210
 
211
+ # Create purchase transaction
212
  transaction = Transaction(
213
  user_id=user.id,
214
  type="purchase",
215
  amount=plan.price,
216
  description=f"Lygos Order: {order_id}",
217
+ status="completed",
218
  )
219
  db.session.add(transaction)
220
 
221
+ # Create success notification
222
  notification = Notification(
223
  user_id=user.id,
224
+ title="Adoption Réussie",
225
+ message=f"Votre adoption de {plan.name} via Lygos a été confirmée. Vous commencerez à recevoir vos gains quotidiens dès demain !",
226
+ type="purchase",
227
  )
228
  db.session.add(notification)
229
+
230
+ # Process referral commission (15% to the referrer)
231
+ process_referral_commission(user, plan.price)
232
+
233
  db.session.commit()
234
 
235
+ flash(
236
+ f"Félicitations ! Votre plan {plan.name} est maintenant actif.",
237
+ "success",
238
+ )
239
+ return redirect(url_for("main.dashboard"))
240
  else:
241
  flash("Cette commande a déjà été traitée.", "info")
242
+ return redirect(url_for("main.dashboard"))
243
 
244
  else:
245
+ flash(
246
+ f"Le paiement n'a pas encore été complété (Statut: {data.get('status')}).",
247
+ "warning",
248
+ )
249
  except Exception as e:
250
  current_app.logger.error(f"Verification Error: {e}")
251
  flash("Erreur lors de la vérification du paiement.", "error")
252
 
253
+ return redirect(url_for("main.market"))
254
+
255
 
256
+ @bp.route("/error")
257
  def error():
258
  flash("Le paiement a échoué ou a été annulé.", "error")
259
+ return redirect(url_for("main.market"))
app/templates/admin/base.html CHANGED
@@ -1,145 +1,201 @@
1
- <!DOCTYPE html>
2
  <html lang="fr">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>{% block title %}Administration{% endblock %} - {{ app_name }}</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
- <style>
10
- body {
11
- font-family: 'Inter', system-ui, -apple-system, sans-serif;
12
- }
13
- .sidebar-link {
14
- transition: all 0.2s ease;
15
- }
16
- .sidebar-link:hover {
17
- background-color: rgba(234, 179, 8, 0.1);
18
- border-left-color: #eab308;
19
- }
20
- .sidebar-link.active {
21
- background-color: rgba(234, 179, 8, 0.15);
22
- border-left-color: #eab308;
23
- color: #eab308;
24
- }
25
- </style>
26
- </head>
27
- <body class="bg-gray-900 text-gray-100 min-h-screen">
28
- <div class="flex min-h-screen">
29
- <!-- Sidebar -->
30
- <aside class="w-64 bg-gray-800 border-r border-gray-700 fixed h-full">
31
- <!-- Logo/Brand -->
32
- <div class="p-6 border-b border-gray-700">
33
- <div class="flex items-center space-x-3">
34
- {% if app_logo %}
35
- <img src="{{ app_logo }}" alt="{{ app_name }}" class="w-10 h-10 rounded-lg">
36
- {% else %}
37
- <div class="w-10 h-10 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-lg flex items-center justify-center">
38
- <i class="fas fa-cog text-white text-lg"></i>
39
- </div>
40
- {% endif %}
41
- <div>
42
- <h1 class="text-lg font-bold text-yellow-400">{{ app_name }}</h1>
43
- <p class="text-xs text-gray-400">Administration</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  </div>
45
  </div>
46
- </div>
47
-
48
- <!-- Navigation -->
49
- <nav class="p-4 space-y-2">
50
- <a href="{{ url_for('admin_panel.dashboard') }}"
51
- class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if request.endpoint == 'admin_panel.dashboard' %}active{% endif %}">
52
- <i class="fas fa-tachometer-alt w-5 mr-3"></i>
53
- Tableau de Bord
54
- </a>
55
 
56
- <a href="{{ url_for('admin_panel.users') }}"
57
- class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if 'users' in request.endpoint %}active{% endif %}">
58
- <i class="fas fa-users w-5 mr-3"></i>
59
- Utilisateurs
60
- </a>
 
 
 
 
61
 
62
- <a href="{{ url_for('admin_panel.withdrawals') }}"
63
- class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if 'withdrawal' in request.endpoint %}active{% endif %}">
64
- <i class="fas fa-money-bill-wave w-5 mr-3"></i>
65
- Retraits
66
- {% if pending_withdrawals_count is defined and pending_withdrawals_count > 0 %}
67
- <span class="ml-auto bg-red-500 text-white text-xs font-bold px-2 py-1 rounded-full">{{ pending_withdrawals_count }}</span>
68
- {% endif %}
69
- </a>
70
 
71
- <a href="{{ url_for('admin_panel.transactions') }}"
72
- class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if 'transactions' in request.endpoint %}active{% endif %}">
73
- <i class="fas fa-exchange-alt w-5 mr-3"></i>
74
- Transactions
75
- </a>
 
 
 
 
 
 
 
 
76
 
77
- <a href="{{ url_for('admin_panel.metals') }}"
78
- class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if 'metals' in request.endpoint %}active{% endif %}">
79
- <i class="fas fa-gem w-5 mr-3"></i>
80
- Plans / Métaux
81
- </a>
 
 
82
 
83
- <div class="pt-4 mt-4 border-t border-gray-700">
84
- <p class="px-4 text-xs text-gray-500 uppercase tracking-wider mb-2">Configuration</p>
 
 
 
 
 
85
 
86
- <a href="{{ url_for('admin_panel.settings') }}"
87
- class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if 'settings' in request.endpoint %}active{% endif %}">
88
- <i class="fas fa-sliders-h w-5 mr-3"></i>
89
- Paramètres Généraux
 
 
90
  </a>
91
- </div>
92
- </nav>
93
 
94
- <!-- Footer -->
95
- <div class="absolute bottom-0 left-0 right-0 p-4 border-t border-gray-700">
96
- <a href="{{ url_for('main.index') }}" class="flex items-center px-4 py-2 text-gray-400 hover:text-yellow-400 transition-colors">
97
- <i class="fas fa-arrow-left w-5 mr-3"></i>
98
- Retour au Site
99
- </a>
100
- </div>
101
- </aside>
102
 
103
- <!-- Main Content -->
104
- <div class="flex-1 ml-64">
105
- <!-- Top Header -->
106
- <header class="bg-gray-800 border-b border-gray-700 px-8 py-4 sticky top-0 z-10">
107
- <div class="flex items-center justify-between">
108
- <h2 class="text-xl font-semibold text-white">
109
- {% block page_title %}Administration{% endblock %}
110
- </h2>
111
- <div class="flex items-center space-x-4">
112
- <span class="text-sm text-gray-400">
113
- <i class="fas fa-shield-alt mr-2 text-green-400"></i>
114
- Accès Administration
115
- </span>
116
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  </div>
118
- </header>
119
 
120
- <!-- Flash Messages -->
121
- {% with messages = get_flashed_messages(with_categories=true) %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  {% if messages %}
123
  <div class="px-8 pt-4">
124
  {% for category, message in messages %}
125
- <div class="mb-4 p-4 rounded-lg {% if category == 'error' %}bg-red-900/50 border border-red-600 text-red-200{% elif category == 'success' %}bg-green-900/50 border border-green-600 text-green-200{% elif category == 'warning' %}bg-yellow-900/50 border border-yellow-600 text-yellow-200{% else %}bg-blue-900/50 border border-blue-600 text-blue-200{% endif %}">
 
 
126
  <div class="flex items-center">
127
- <i class="fas {% if category == 'error' %}fa-exclamation-circle{% elif category == 'success' %}fa-check-circle{% elif category == 'warning' %}fa-exclamation-triangle{% else %}fa-info-circle{% endif %} mr-3"></i>
 
 
128
  {{ message }}
129
  </div>
130
  </div>
131
  {% endfor %}
132
  </div>
133
- {% endif %}
134
- {% endwith %}
135
 
136
- <!-- Page Content -->
137
- <main class="p-8">
138
- {% block content %}{% endblock %}
139
- </main>
140
  </div>
141
- </div>
142
 
143
- {% block scripts %}{% endblock %}
144
- </body>
145
  </html>
 
1
+ <!doctype html>
2
  <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>
7
+ {% block title %}Administration{% endblock %} - {{ app_name }}
8
+ </title>
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <link
11
+ rel="stylesheet"
12
+ href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
13
+ />
14
+ <style>
15
+ body {
16
+ font-family:
17
+ "Inter",
18
+ system-ui,
19
+ -apple-system,
20
+ sans-serif;
21
+ }
22
+ .sidebar-link {
23
+ transition: all 0.2s ease;
24
+ }
25
+ .sidebar-link:hover {
26
+ background-color: rgba(234, 179, 8, 0.1);
27
+ border-left-color: #eab308;
28
+ }
29
+ .sidebar-link.active {
30
+ background-color: rgba(234, 179, 8, 0.15);
31
+ border-left-color: #eab308;
32
+ color: #eab308;
33
+ }
34
+ </style>
35
+ </head>
36
+ <body class="bg-gray-900 text-gray-100 min-h-screen">
37
+ <div class="flex min-h-screen">
38
+ <!-- Sidebar -->
39
+ <aside
40
+ class="w-64 bg-gray-800 border-r border-gray-700 fixed h-full"
41
+ >
42
+ <!-- Logo/Brand -->
43
+ <div class="p-6 border-b border-gray-700">
44
+ <div class="flex items-center space-x-3">
45
+ {% if app_logo %}
46
+ <img
47
+ src="{{ app_logo }}"
48
+ alt="{{ app_name }}"
49
+ class="w-10 h-10 rounded-lg"
50
+ />
51
+ {% else %}
52
+ <div
53
+ class="w-10 h-10 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-lg flex items-center justify-center"
54
+ >
55
+ <i class="fas fa-cog text-white text-lg"></i>
56
+ </div>
57
+ {% endif %}
58
+ <div>
59
+ <h1 class="text-lg font-bold text-yellow-400">
60
+ {{ app_name }}
61
+ </h1>
62
+ <p class="text-xs text-gray-400">Administration</p>
63
+ </div>
64
  </div>
65
  </div>
 
 
 
 
 
 
 
 
 
66
 
67
+ <!-- Navigation -->
68
+ <nav class="p-4 space-y-2">
69
+ <a
70
+ href="{{ url_for('admin_panel.dashboard') }}"
71
+ class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if request.endpoint == 'admin_panel.dashboard' %}active{% endif %}"
72
+ >
73
+ <i class="fas fa-tachometer-alt w-5 mr-3"></i>
74
+ Tableau de Bord
75
+ </a>
76
 
77
+ <a
78
+ href="{{ url_for('admin_panel.users') }}"
79
+ class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if 'users' in request.endpoint or 'user_detail' in request.endpoint %}active{% endif %}"
80
+ >
81
+ <i class="fas fa-users w-5 mr-3"></i>
82
+ Utilisateurs
83
+ </a>
 
84
 
85
+ <a
86
+ href="{{ url_for('admin_panel.withdrawals') }}"
87
+ class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if 'withdrawal' in request.endpoint %}active{% endif %}"
88
+ >
89
+ <i class="fas fa-money-bill-wave w-5 mr-3"></i>
90
+ Retraits {% if pending_withdrawals_count is defined and
91
+ pending_withdrawals_count > 0 %}
92
+ <span
93
+ class="ml-auto bg-red-500 text-white text-xs font-bold px-2 py-1 rounded-full"
94
+ >{{ pending_withdrawals_count }}</span
95
+ >
96
+ {% endif %}
97
+ </a>
98
 
99
+ <a
100
+ href="{{ url_for('admin_panel.transactions') }}"
101
+ class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if request.endpoint == 'admin_panel.transactions' %}active{% endif %}"
102
+ >
103
+ <i class="fas fa-exchange-alt w-5 mr-3"></i>
104
+ Transactions
105
+ </a>
106
 
107
+ <a
108
+ href="{{ url_for('admin_panel.referral_commissions') }}"
109
+ class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if 'referral_commissions' in request.endpoint %}active{% endif %}"
110
+ >
111
+ <i class="fas fa-share-nodes w-5 mr-3"></i>
112
+ Commissions Parrainage
113
+ </a>
114
 
115
+ <a
116
+ href="{{ url_for('admin_panel.metals') }}"
117
+ class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if 'metals' in request.endpoint %}active{% endif %}"
118
+ >
119
+ <i class="fas fa-gem w-5 mr-3"></i>
120
+ Plans / Métaux
121
  </a>
 
 
122
 
123
+ <div class="pt-4 mt-4 border-t border-gray-700">
124
+ <p
125
+ class="px-4 text-xs text-gray-500 uppercase tracking-wider mb-2"
126
+ >
127
+ Configuration
128
+ </p>
 
 
129
 
130
+ <a
131
+ href="{{ url_for('admin_panel.settings') }}"
132
+ class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if 'settings' in request.endpoint %}active{% endif %}"
133
+ >
134
+ <i class="fas fa-sliders-h w-5 mr-3"></i>
135
+ Paramètres Généraux
136
+ </a>
 
 
 
 
 
 
137
  </div>
138
+ </nav>
139
+
140
+ <!-- Footer -->
141
+ <div
142
+ class="absolute bottom-0 left-0 right-0 p-4 border-t border-gray-700"
143
+ >
144
+ <a
145
+ href="{{ url_for('main.index') }}"
146
+ class="flex items-center px-4 py-2 text-gray-400 hover:text-yellow-400 transition-colors"
147
+ >
148
+ <i class="fas fa-arrow-left w-5 mr-3"></i>
149
+ Retour au Site
150
+ </a>
151
  </div>
152
+ </aside>
153
 
154
+ <!-- Main Content -->
155
+ <div class="flex-1 ml-64">
156
+ <!-- Top Header -->
157
+ <header
158
+ class="bg-gray-800 border-b border-gray-700 px-8 py-4 sticky top-0 z-10"
159
+ >
160
+ <div class="flex items-center justify-between">
161
+ <h2 class="text-xl font-semibold text-white">
162
+ {% block page_title %}Administration{% endblock %}
163
+ </h2>
164
+ <div class="flex items-center space-x-4">
165
+ <span class="text-sm text-gray-400">
166
+ <i
167
+ class="fas fa-shield-alt mr-2 text-green-400"
168
+ ></i>
169
+ Accès Administration
170
+ </span>
171
+ </div>
172
+ </div>
173
+ </header>
174
+
175
+ <!-- Flash Messages -->
176
+ {% with messages = get_flashed_messages(with_categories=true) %}
177
  {% if messages %}
178
  <div class="px-8 pt-4">
179
  {% for category, message in messages %}
180
+ <div
181
+ class="mb-4 p-4 rounded-lg {% if category == 'error' %}bg-red-900/50 border border-red-600 text-red-200{% elif category == 'success' %}bg-green-900/50 border border-green-600 text-green-200{% elif category == 'warning' %}bg-yellow-900/50 border border-yellow-600 text-yellow-200{% else %}bg-blue-900/50 border border-blue-600 text-blue-200{% endif %}"
182
+ >
183
  <div class="flex items-center">
184
+ <i
185
+ class="fas {% if category == 'error' %}fa-exclamation-circle{% elif category == 'success' %}fa-check-circle{% elif category == 'warning' %}fa-exclamation-triangle{% else %}fa-info-circle{% endif %} mr-3"
186
+ ></i>
187
  {{ message }}
188
  </div>
189
  </div>
190
  {% endfor %}
191
  </div>
192
+ {% endif %} {% endwith %}
 
193
 
194
+ <!-- Page Content -->
195
+ <main class="p-8">{% block content %}{% endblock %}</main>
196
+ </div>
 
197
  </div>
 
198
 
199
+ {% block scripts %}{% endblock %}
200
+ </body>
201
  </html>
app/templates/admin/dashboard.html CHANGED
@@ -1,20 +1,113 @@
1
- {% extends "admin/base.html" %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- {% block title %}Tableau de Bord{% endblock %}
4
- {% block page_title %}Tableau de Bord{% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- {% block content %}
7
- <!-- Stats Cards -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
9
- <!-- Total Users -->
10
  <div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
11
  <div class="flex items-center justify-between">
12
  <div>
13
- <p class="text-gray-400 text-sm">Utilisateurs Total</p>
14
- <h3 class="text-3xl font-bold text-white mt-1">{{ total_users }}</h3>
 
 
15
  </div>
16
- <div class="w-12 h-12 bg-blue-500/20 rounded-lg flex items-center justify-center">
17
- <i class="fas fa-users text-blue-400 text-xl"></i>
 
 
18
  </div>
19
  </div>
20
  <p class="text-sm text-gray-500 mt-3">
@@ -27,52 +120,111 @@
27
  <div class="flex items-center justify-between">
28
  <div>
29
  <p class="text-gray-400 text-sm">Retraits en Attente</p>
30
- <h3 class="text-3xl font-bold text-white mt-1">{{ pending_withdrawals }}</h3>
 
 
31
  </div>
32
- <div class="w-12 h-12 bg-yellow-500/20 rounded-lg flex items-center justify-center">
33
- <i class="fas fa-clock text-yellow-400 text-xl"></i>
 
 
34
  </div>
35
  </div>
36
- <a href="{{ url_for('admin_panel.withdrawals') }}" class="text-sm text-yellow-400 hover:text-yellow-300 mt-3 inline-block">
 
 
 
37
  Voir les demandes <i class="fas fa-arrow-right ml-1"></i>
38
  </a>
39
  </div>
40
 
41
- <!-- Total Deposits -->
42
  <div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
43
  <div class="flex items-center justify-between">
44
  <div>
45
- <p class="text-gray-400 text-sm">Total Dépôts</p>
46
- <h3 class="text-2xl font-bold text-white mt-1">{{ "{:,.0f}".format(total_deposits) }} <span class="text-sm font-normal text-gray-400">FCFA</span></h3>
 
 
 
47
  </div>
48
- <div class="w-12 h-12 bg-green-500/20 rounded-lg flex items-center justify-center">
49
- <i class="fas fa-arrow-down text-green-400 text-xl"></i>
 
 
50
  </div>
51
  </div>
 
52
  </div>
53
 
54
- <!-- Total Investments -->
55
  <div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
56
  <div class="flex items-center justify-between">
57
  <div>
58
- <p class="text-gray-400 text-sm">Investissements Actifs</p>
59
- <h3 class="text-2xl font-bold text-white mt-1">{{ "{:,.0f}".format(total_investments) }} <span class="text-sm font-normal text-gray-400">FCFA</span></h3>
 
 
 
60
  </div>
61
- <div class="w-12 h-12 bg-purple-500/20 rounded-lg flex items-center justify-center">
62
- <i class="fas fa-chart-line text-purple-400 text-xl"></i>
 
 
63
  </div>
64
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  </div>
66
  </div>
67
 
68
  <!-- Recent Transactions -->
69
  <div class="bg-gray-800 rounded-xl border border-gray-700">
70
- <div class="px-6 py-4 border-b border-gray-700 flex items-center justify-between">
 
 
71
  <h3 class="text-lg font-semibold text-white">
72
  <i class="fas fa-history mr-2 text-yellow-400"></i>
73
  Transactions Récentes
74
  </h3>
75
- <a href="{{ url_for('admin_panel.transactions') }}" class="text-sm text-yellow-400 hover:text-yellow-300">
 
 
 
76
  Voir tout <i class="fas fa-arrow-right ml-1"></i>
77
  </a>
78
  </div>
@@ -81,65 +233,104 @@
81
  <table class="w-full">
82
  <thead class="bg-gray-900/50">
83
  <tr>
84
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Utilisateur</th>
85
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Type</th>
86
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Montant</th>
87
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Statut</th>
88
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Date</th>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  </tr>
90
  </thead>
91
  <tbody class="divide-y divide-gray-700">
92
  {% for transaction in recent_transactions %}
93
  <tr class="hover:bg-gray-700/50 transition-colors">
94
  <td class="px-6 py-4 whitespace-nowrap">
95
- <span class="text-gray-300">{{ transaction.user.phone if transaction.user else 'N/A' }}</span>
 
 
 
96
  </td>
97
  <td class="px-6 py-4 whitespace-nowrap">
98
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
99
- {% if transaction.type == 'deposit' %}bg-green-500/20 text-green-400
100
- {% elif transaction.type == 'withdrawal' %}bg-red-500/20 text-red-400
101
- {% elif transaction.type == 'purchase' %}bg-blue-500/20 text-blue-400
102
- {% elif transaction.type == 'bonus' %}bg-yellow-500/20 text-yellow-400
103
- {% else %}bg-gray-500/20 text-gray-400{% endif %}">
104
  {% if transaction.type == 'deposit' %}
105
- <i class="fas fa-arrow-down mr-1"></i> Dépôt
106
- {% elif transaction.type == 'withdrawal' %}
107
- <i class="fas fa-arrow-up mr-1"></i> Retrait
108
- {% elif transaction.type == 'purchase' %}
109
- <i class="fas fa-shopping-cart mr-1"></i> Achat
110
- {% elif transaction.type == 'bonus' %}
111
- <i class="fas fa-gift mr-1"></i> Bonus
112
- {% else %}
113
- {{ transaction.type }}
114
- {% endif %}
115
  </span>
116
  </td>
117
  <td class="px-6 py-4 whitespace-nowrap">
118
- <span class="font-medium {% if transaction.type == 'withdrawal' %}text-red-400{% else %}text-green-400{% endif %}">
119
- {% if transaction.type == 'withdrawal' %}-{% else %}+{% endif %}{{ "{:,.0f}".format(transaction.amount) }} FCFA
 
 
 
 
120
  </span>
121
  </td>
122
  <td class="px-6 py-4 whitespace-nowrap">
123
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
124
- {% if transaction.status == 'completed' %}bg-green-500/20 text-green-400
125
- {% elif transaction.status == 'pending' %}bg-yellow-500/20 text-yellow-400
126
- {% elif transaction.status == 'approved' %}bg-blue-500/20 text-blue-400
127
- {% elif transaction.status == 'rejected' %}bg-red-500/20 text-red-400
128
- {% else %}bg-gray-500/20 text-gray-400{% endif %}">
 
 
 
 
 
 
 
 
129
  {% if transaction.status == 'completed' %}Complété
130
  {% elif transaction.status == 'pending' %}En attente
131
  {% elif transaction.status == 'approved' %}Approuvé
132
- {% elif transaction.status == 'rejected' %}Rejeté
133
- {% else %}{{ transaction.status }}{% endif %}
134
  </span>
135
  </td>
136
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
 
 
137
  {{ transaction.created_at.strftime('%d/%m/%Y %H:%M') }}
138
  </td>
139
  </tr>
140
  {% else %}
141
  <tr>
142
- <td colspan="5" class="px-6 py-8 text-center text-gray-500">
143
  <i class="fas fa-inbox text-4xl mb-3"></i>
144
  <p>Aucune transaction récente</p>
145
  </td>
@@ -148,5 +339,5 @@
148
  </tbody>
149
  </table>
150
  </div>
151
- </div></p>
152
  {% endblock %}
 
1
+ {% extends "admin/base.html" %} {% block title %}Tableau de Bord{% endblock %}
2
+ {% block page_title %}Tableau de Bord{% endblock %} {% block content %}
3
+ <!-- Stats Cards - Row 1 -->
4
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
5
+ <!-- Total Users (Displayed) -->
6
+ <div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
7
+ <div class="flex items-center justify-between">
8
+ <div>
9
+ <p class="text-gray-400 text-sm">Utilisateurs Affichés</p>
10
+ <h3 class="text-3xl font-bold text-white mt-1">
11
+ {{ "{:,}".format(displayed_user_count) }}
12
+ </h3>
13
+ </div>
14
+ <div
15
+ class="w-12 h-12 bg-blue-500/20 rounded-lg flex items-center justify-center"
16
+ >
17
+ <i class="fas fa-users text-blue-400 text-xl"></i>
18
+ </div>
19
+ </div>
20
+ <p class="text-sm text-gray-500 mt-3">
21
+ <span class="text-green-400">{{ total_users }}</span> réels +
22
+ <span class="text-yellow-400">{{ fake_user_count }}</span> ajoutés
23
+ </p>
24
+ </div>
25
 
26
+ <!-- Total Deposits -->
27
+ <div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
28
+ <div class="flex items-center justify-between">
29
+ <div>
30
+ <p class="text-gray-400 text-sm">Total Dépôts</p>
31
+ <h3 class="text-2xl font-bold text-white mt-1">
32
+ {{ "{:,.0f}".format(total_deposits) }}
33
+ <span class="text-sm font-normal text-gray-400">FCFA</span>
34
+ </h3>
35
+ </div>
36
+ <div
37
+ class="w-12 h-12 bg-green-500/20 rounded-lg flex items-center justify-center"
38
+ >
39
+ <i class="fas fa-arrow-down text-green-400 text-xl"></i>
40
+ </div>
41
+ </div>
42
+ <p class="text-sm text-gray-500 mt-3">
43
+ Somme de tous les achats de plans
44
+ </p>
45
+ </div>
46
 
47
+ <!-- Total Withdrawals -->
48
+ <div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
49
+ <div class="flex items-center justify-between">
50
+ <div>
51
+ <p class="text-gray-400 text-sm">Total Retraits</p>
52
+ <h3 class="text-2xl font-bold text-white mt-1">
53
+ {{ "{:,.0f}".format(total_withdrawals) }}
54
+ <span class="text-sm font-normal text-gray-400">FCFA</span>
55
+ </h3>
56
+ </div>
57
+ <div
58
+ class="w-12 h-12 bg-red-500/20 rounded-lg flex items-center justify-center"
59
+ >
60
+ <i class="fas fa-arrow-up text-red-400 text-xl"></i>
61
+ </div>
62
+ </div>
63
+ <p class="text-sm text-gray-500 mt-3">
64
+ Montant brut des retraits effectués
65
+ </p>
66
+ </div>
67
+
68
+ <!-- Total Withdrawal Fees -->
69
+ <div
70
+ class="bg-gray-800 rounded-xl p-6 border border-gray-700 relative overflow-hidden"
71
+ >
72
+ <div
73
+ class="absolute top-0 right-0 w-20 h-20 bg-yellow-500/10 rounded-full blur-xl -mr-10 -mt-10"
74
+ ></div>
75
+ <div class="flex items-center justify-between relative z-10">
76
+ <div>
77
+ <p class="text-gray-400 text-sm">Frais Collectés (15%)</p>
78
+ <h3 class="text-2xl font-bold text-yellow-400 mt-1">
79
+ {{ "{:,.0f}".format(total_withdrawal_fees) }}
80
+ <span class="text-sm font-normal text-gray-400">FCFA</span>
81
+ </h3>
82
+ </div>
83
+ <div
84
+ class="w-12 h-12 bg-yellow-500/20 rounded-lg flex items-center justify-center"
85
+ >
86
+ <i class="fas fa-coins text-yellow-400 text-xl"></i>
87
+ </div>
88
+ </div>
89
+ <p class="text-sm text-gray-500 mt-3">
90
+ <i class="fas fa-chart-line text-green-400 mr-1"></i>
91
+ Revenus sur les retraits
92
+ </p>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- Stats Cards - Row 2 -->
97
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
98
+ <!-- Real Users -->
99
  <div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
100
  <div class="flex items-center justify-between">
101
  <div>
102
+ <p class="text-gray-400 text-sm">Utilisateurs Réels</p>
103
+ <h3 class="text-3xl font-bold text-white mt-1">
104
+ {{ total_users }}
105
+ </h3>
106
  </div>
107
+ <div
108
+ class="w-12 h-12 bg-indigo-500/20 rounded-lg flex items-center justify-center"
109
+ >
110
+ <i class="fas fa-user-check text-indigo-400 text-xl"></i>
111
  </div>
112
  </div>
113
  <p class="text-sm text-gray-500 mt-3">
 
120
  <div class="flex items-center justify-between">
121
  <div>
122
  <p class="text-gray-400 text-sm">Retraits en Attente</p>
123
+ <h3 class="text-3xl font-bold text-white mt-1">
124
+ {{ pending_withdrawals }}
125
+ </h3>
126
  </div>
127
+ <div
128
+ class="w-12 h-12 bg-orange-500/20 rounded-lg flex items-center justify-center"
129
+ >
130
+ <i class="fas fa-clock text-orange-400 text-xl"></i>
131
  </div>
132
  </div>
133
+ <a
134
+ href="{{ url_for('admin_panel.withdrawals') }}"
135
+ class="text-sm text-orange-400 hover:text-orange-300 mt-3 inline-block"
136
+ >
137
  Voir les demandes <i class="fas fa-arrow-right ml-1"></i>
138
  </a>
139
  </div>
140
 
141
+ <!-- Active Investments -->
142
  <div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
143
  <div class="flex items-center justify-between">
144
  <div>
145
+ <p class="text-gray-400 text-sm">Investissements Actifs</p>
146
+ <h3 class="text-2xl font-bold text-white mt-1">
147
+ {{ "{:,.0f}".format(total_investments) }}
148
+ <span class="text-sm font-normal text-gray-400">FCFA</span>
149
+ </h3>
150
  </div>
151
+ <div
152
+ class="w-12 h-12 bg-purple-500/20 rounded-lg flex items-center justify-center"
153
+ >
154
+ <i class="fas fa-chart-line text-purple-400 text-xl"></i>
155
  </div>
156
  </div>
157
+ <p class="text-sm text-gray-500 mt-3">Valeur des plans actifs</p>
158
  </div>
159
 
160
+ <!-- Referral Commissions -->
161
  <div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
162
  <div class="flex items-center justify-between">
163
  <div>
164
+ <p class="text-gray-400 text-sm">Commissions Parrainage</p>
165
+ <h3 class="text-2xl font-bold text-white mt-1">
166
+ {{ "{:,.0f}".format(total_referral_commissions) }}
167
+ <span class="text-sm font-normal text-gray-400">FCFA</span>
168
+ </h3>
169
  </div>
170
+ <div
171
+ class="w-12 h-12 bg-pink-500/20 rounded-lg flex items-center justify-center"
172
+ >
173
+ <i class="fas fa-share-nodes text-pink-400 text-xl"></i>
174
  </div>
175
  </div>
176
+ <a
177
+ href="{{ url_for('admin_panel.referral_commissions') }}"
178
+ class="text-sm text-pink-400 hover:text-pink-300 mt-3 inline-block"
179
+ >
180
+ Voir les détails <i class="fas fa-arrow-right ml-1"></i>
181
+ </a>
182
+ </div>
183
+ </div>
184
+
185
+ <!-- Summary Card -->
186
+ <div
187
+ class="bg-gradient-to-r from-yellow-500/10 to-amber-500/10 rounded-xl p-6 border border-yellow-500/30 mb-8"
188
+ >
189
+ <h3 class="text-lg font-bold text-white mb-4">
190
+ <i class="fas fa-chart-pie mr-2 text-yellow-400"></i>
191
+ Résumé Financier
192
+ </h3>
193
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
194
+ <div class="text-center p-4 bg-gray-800/50 rounded-lg">
195
+ <p class="text-gray-400 text-sm mb-2">Total Entrant</p>
196
+ <p class="text-2xl font-bold text-green-400">
197
+ {{ "{:,.0f}".format(total_deposits) }} FCFA
198
+ </p>
199
+ </div>
200
+ <div class="text-center p-4 bg-gray-800/50 rounded-lg">
201
+ <p class="text-gray-400 text-sm mb-2">Total Sortant</p>
202
+ <p class="text-2xl font-bold text-red-400">
203
+ {{ "{:,.0f}".format(total_withdrawals) }} FCFA
204
+ </p>
205
+ </div>
206
+ <div class="text-center p-4 bg-gray-800/50 rounded-lg">
207
+ <p class="text-gray-400 text-sm mb-2">Gains (Frais 15%)</p>
208
+ <p class="text-2xl font-bold text-yellow-400">
209
+ {{ "{:,.0f}".format(total_withdrawal_fees) }} FCFA
210
+ </p>
211
+ </div>
212
  </div>
213
  </div>
214
 
215
  <!-- Recent Transactions -->
216
  <div class="bg-gray-800 rounded-xl border border-gray-700">
217
+ <div
218
+ class="px-6 py-4 border-b border-gray-700 flex items-center justify-between"
219
+ >
220
  <h3 class="text-lg font-semibold text-white">
221
  <i class="fas fa-history mr-2 text-yellow-400"></i>
222
  Transactions Récentes
223
  </h3>
224
+ <a
225
+ href="{{ url_for('admin_panel.transactions') }}"
226
+ class="text-sm text-yellow-400 hover:text-yellow-300"
227
+ >
228
  Voir tout <i class="fas fa-arrow-right ml-1"></i>
229
  </a>
230
  </div>
 
233
  <table class="w-full">
234
  <thead class="bg-gray-900/50">
235
  <tr>
236
+ <th
237
+ class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider"
238
+ >
239
+ Utilisateur
240
+ </th>
241
+ <th
242
+ class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider"
243
+ >
244
+ Type
245
+ </th>
246
+ <th
247
+ class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider"
248
+ >
249
+ Montant
250
+ </th>
251
+ <th
252
+ class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider"
253
+ >
254
+ Frais
255
+ </th>
256
+ <th
257
+ class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider"
258
+ >
259
+ Statut
260
+ </th>
261
+ <th
262
+ class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider"
263
+ >
264
+ Date
265
+ </th>
266
  </tr>
267
  </thead>
268
  <tbody class="divide-y divide-gray-700">
269
  {% for transaction in recent_transactions %}
270
  <tr class="hover:bg-gray-700/50 transition-colors">
271
  <td class="px-6 py-4 whitespace-nowrap">
272
+ <span class="text-gray-300"
273
+ >{{ transaction.user.phone if transaction.user else
274
+ 'N/A' }}</span
275
+ >
276
  </td>
277
  <td class="px-6 py-4 whitespace-nowrap">
278
+ <span
279
+ class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {% if transaction.type == 'deposit' %}bg-green-500/20 text-green-400 {% elif transaction.type == 'withdrawal' %}bg-red-500/20 text-red-400 {% elif transaction.type == 'purchase' %}bg-blue-500/20 text-blue-400 {% elif transaction.type == 'bonus' %}bg-yellow-500/20 text-yellow-400 {% elif transaction.type == 'referral' %}bg-pink-500/20 text-pink-400 {% else %}bg-gray-500/20 text-gray-400{% endif %}"
280
+ >
 
 
 
281
  {% if transaction.type == 'deposit' %}
282
+ <i class="fas fa-arrow-down mr-1"></i> Dépôt {% elif
283
+ transaction.type == 'withdrawal' %}
284
+ <i class="fas fa-arrow-up mr-1"></i> Retrait {% elif
285
+ transaction.type == 'purchase' %}
286
+ <i class="fas fa-shopping-cart mr-1"></i> Achat {%
287
+ elif transaction.type == 'bonus' %}
288
+ <i class="fas fa-gift mr-1"></i> Bonus {% elif
289
+ transaction.type == 'referral' %}
290
+ <i class="fas fa-share-nodes mr-1"></i> Parrainage
291
+ {% else %} {{ transaction.type }} {% endif %}
292
  </span>
293
  </td>
294
  <td class="px-6 py-4 whitespace-nowrap">
295
+ <span
296
+ class="font-medium {% if transaction.type == 'withdrawal' %}text-red-400{% else %}text-green-400{% endif %}"
297
+ >
298
+ {% if transaction.type == 'withdrawal' %}-{% else
299
+ %}+{% endif %}{{
300
+ "{:,.0f}".format(transaction.amount) }} FCFA
301
  </span>
302
  </td>
303
  <td class="px-6 py-4 whitespace-nowrap">
304
+ {% if transaction.type == 'withdrawal' and
305
+ transaction.fee_amount %}
306
+ <span class="text-yellow-400 font-medium"
307
+ >{{ "{:,.0f}".format(transaction.fee_amount) }}
308
+ FCFA</span
309
+ >
310
+ {% else %}
311
+ <span class="text-gray-500">-</span>
312
+ {% endif %}
313
+ </td>
314
+ <td class="px-6 py-4 whitespace-nowrap">
315
+ <span
316
+ class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {% if transaction.status == 'completed' %}bg-green-500/20 text-green-400 {% elif transaction.status == 'pending' %}bg-yellow-500/20 text-yellow-400 {% elif transaction.status == 'approved' %}bg-blue-500/20 text-blue-400 {% elif transaction.status == 'rejected' %}bg-red-500/20 text-red-400 {% else %}bg-gray-500/20 text-gray-400{% endif %}"
317
+ >
318
  {% if transaction.status == 'completed' %}Complété
319
  {% elif transaction.status == 'pending' %}En attente
320
  {% elif transaction.status == 'approved' %}Approuvé
321
+ {% elif transaction.status == 'rejected' %}Rejeté {%
322
+ else %}{{ transaction.status }}{% endif %}
323
  </span>
324
  </td>
325
+ <td
326
+ class="px-6 py-4 whitespace-nowrap text-sm text-gray-400"
327
+ >
328
  {{ transaction.created_at.strftime('%d/%m/%Y %H:%M') }}
329
  </td>
330
  </tr>
331
  {% else %}
332
  <tr>
333
+ <td colspan="6" class="px-6 py-8 text-center text-gray-500">
334
  <i class="fas fa-inbox text-4xl mb-3"></i>
335
  <p>Aucune transaction récente</p>
336
  </td>
 
339
  </tbody>
340
  </table>
341
  </div>
342
+ </div>
343
  {% endblock %}
app/templates/admin/referral_commissions.html ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "admin/base.html" %}
2
+
3
+ {% block title %}Commissions de Parrainage{% endblock %}
4
+ {% block page_title %}Commissions de Parrainage{% endblock %}
5
+
6
+ {% block content %}
7
+ <!-- Header -->
8
+ <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
9
+ <div>
10
+ <h1 class="text-2xl font-bold text-white</div>">
11
+ <i class="fas fa-share-nodes mr-3 text-pink-400"></i>
12
+ Commissions de Parrainage
13
+ </h1>
14
+ <p class="text-gray-400 text-sm mt-1">
15
+ Suivi des commissions versées aux parrains (15% sur achats + 3% sur gains)
16
+ </p>
17
+ </div>
18
+ </div>
19
+
20
+ <!-- Filter Buttons -->
21
+ <div class="flex flex-wrap items-center gap-2 mb-6">
22
+ <a href="{{ url_for('admin_panel.referral_commissions') }}"
23
+ class="px-4 py-2 rounded-lg font-medium transition-colors {% if not request.args.get('type') %}bg-pink-600 text-white{% else %}bg-gray-700 text-gray-300 hover:bg-gray-600{% endif %}">
24
+ Toutes
25
+ </a>
26
+ <a href="{{ url_for('admin_panel.referral_commissions', type='purchase') }}"
27
+ class="px-4 py-2 rounded-lg font-medium transition-colors {% if request.args.get('type') == 'purchase' %}bg-yellow-600 text-white{% else %}bg-gray-700 text-gray-300 hover:bg-gray-600{% endif %}">
28
+ <i class="fas fa-shopping-cart mr-1"></i> Achats (15%)
29
+ </a>
30
+ <a href="{{ url_for('admin_panel.referral_commissions', type='daily_gain') }}"
31
+ class="px-4 py-2 rounded-lg font-medium transition-colors {% if request.args.get('type') == 'daily_gain' %}bg-green-600 text-white{% else %}bg-gray-700 text-gray-300 hover:bg-gray-600{% endif %}">
32
+ <i class="fas fa-chart-line mr-1"></i> Gains Quotidiens (3%)
33
+ </a>
34
+ </div>
35
+
36
+ <!-- Stats Cards -->
37
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
38
+ <div class="bg-gray-800 rounded-lg border border-gray-700 p-4">
39
+ <div class="flex items-center justify-between">
40
+ <div>
41
+ <p class="text-gray-400 text-sm">Commissions sur Achats</p>
42
+ <p class="text-2xl font-bold text-yellow-400">{{ "{:,.0f}".format(total_purchase_commissions) }} <span class="text-sm text-gray-400">FCFA</span></p>
43
+ </div>
44
+ <div class="w-10 h-10 bg-yellow-500/20 rounded-lg flex items-center justify-center">
45
+ <i class="fas fa-shopping-cart text-yellow-400"></i>
46
+ </div>
47
+ </div>
48
+ <p class="text-xs text-gray-500 mt-2">15% sur chaque achat de plan des filleuls</p>
49
+ </div>
50
+ <div class="bg-gray-800 rounded-lg border border-gray-700 p-4">
51
+ <div class="flex items-center justify-between">
52
+ <div>
53
+ <p class="text-gray-400 text-sm">Commissions sur Gains</p>
54
+ <p class="text-2xl font-bold text-green-400">{{ "{:,.0f}".format(total_daily_commissions) }} <span class="text-sm text-gray-400">FCFA</span></p>
55
+ </div>
56
+ <div class="w-10 h-10 bg-green-500/20 rounded-lg flex items-center justify-center">
57
+ <i class="fas fa-chart-line text-green-400"></i>
58
+ </div>
59
+ </div>
60
+ <p class="text-xs text-gray-500 mt-2">3% sur les gains quotidiens des filleuls</p>
61
+ </div>
62
+ <div class="bg-gradient-to-br from-pink-500/20 to-purple-500/20 rounded-lg border border-pink-500/30 p-4">
63
+ <div class="flex items-center justify-between">
64
+ <div>
65
+ <p class="text-gray-400 text-sm">Total Commissions Versées</p>
66
+ <p class="text-2xl font-bold text-pink-400">{{ "{:,.0f}".format(total_commissions) }} <span class="text-sm text-gray-400">FCFA</span></p>
67
+ </div>
68
+ <div class="w-10 h-10 bg-pink-500/20 rounded-lg flex items-center justify-center">
69
+ <i class="fas fa-coins text-pink-400"></i>
70
+ </div>
71
+ </div>
72
+ <p class="text-xs text-gray-500 mt-2">Somme de toutes les commissions de parrainage</p>
73
+ </div>
74
+ </div>
75
+
76
+ <!-- Commissions Table -->
77
+ <div class="bg-gray-800 rounded-xl border border-gray-700 overflow-hidden">
78
+ <div class="overflow-x-auto">
79
+ <table class="w-full">
80
+ <thead class="bg-gray-900/50">
81
+ <tr>
82
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">ID</th>
83
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Parrain</th>
84
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Filleul</th>
85
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Type</th>
86
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Base</th>
87
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Taux</th>
88
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Commission</th>
89
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Date</th>
90
+ </tr>
91
+ </thead>
92
+ <tbody class="divide-y divide-gray-700">
93
+ {% for commission in commissions.items %}
94
+ <tr class="hover:bg-gray-700/50 transition-colors">
95
+ <td class="px-4 py-4 whitespace-nowrap text-sm text-gray-400">
96
+ #{{ commission.id }}
97
+ </td>
98
+ <td class="px-4 py-4 whitespace-nowrap">
99
+ <div class="flex items-center">
100
+ <div class="w-8 h-8 bg-gradient-to-br from-pink-400 to-pink-600 rounded-full flex items-center justify-center">
101
+ <span class="text-white font-bold text-xs">{{ commission.referrer.name[0].upper() if commission.referrer and commission.referrer.name else '?' }}</span>
102
+ </div>
103
+ <div class="ml-2">
104
+ <p class="text-white text-sm font-medium">{{ commission.referrer.name if commission.referrer else 'N/A' }}</p>
105
+ <p class="text-xs text-gray-400">{{ commission.referrer.phone if commission.referrer else '' }}</p>
106
+ </div>
107
+ </div>
108
+ </td>
109
+ <td class="px-4 py-4 whitespace-nowrap">
110
+ <div class="flex items-center">
111
+ <div class="w-8 h-8 bg-gradient-to-br from-blue-400 to-blue-600 rounded-full flex items-center justify-center">
112
+ <span class="text-white font-bold text-xs">{{ commission.referred_user.name[0].upper() if commission.referred_user and commission.referred_user.name else '?' }}</span>
113
+ </div>
114
+ <div class="ml-2">
115
+ <p class="text-white text-sm font-medium">{{ commission.referred_user.name if commission.referred_user else 'N/A' }}</p>
116
+ <p class="text-xs text-gray-400">{{ commission.referred_user.phone if commission.referred_user else '' }}</p>
117
+ </div>
118
+ </div>
119
+ </td>
120
+ <td class="px-4 py-4 whitespace-nowrap">
121
+ {% if commission.commission_type == 'purchase' %}
122
+ <span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-yellow-500/20 text-yellow-400 border border-yellow-500/30">
123
+ <i class="fas fa-shopping-cart mr-1.5"></i> Achat
124
+ </span>
125
+ {% elif commission.commission_type == 'daily_gain' %}
126
+ <span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-green-500/20 text-green-400 border border-green-500/30">
127
+ <i class="fas fa-chart-line mr-1.5"></i> Gain
128
+ </span>
129
+ {% else %}
130
+ <span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-gray-500/20 text-gray-400 border border-gray-500/30">
131
+ {{ commission.commission_type }}
132
+ </span>
133
+ {% endif %}
134
+ </td>
135
+ <td class="px-4 py-4 whitespace-nowrap">
136
+ {% if commission.commission_type == 'purchase' and commission.purchase_amount %}
137
+ <span class="text-gray-300">{{ "{:,.0f}".format(commission.purchase_amount) }} <span class="text-xs text-gray-500">FCFA</span></span>
138
+ {% elif commission.commission_type == 'daily_gain' and commission.gain_amount %}
139
+ <span class="text-gray-300">{{ "{:,.0f}".format(commission.gain_amount) }} <span class="text-xs text-gray-500">FCFA</span></span>
140
+ {% else %}
141
+ <span class="text-gray-500">-</span>
142
+ {% endif %}
143
+ </td>
144
+ <td class="px-4 py-4 whitespace-nowrap">
145
+ <span class="text-purple-400 font-medium">{{ commission.commission_percentage }}%</span>
146
+ </td>
147
+ <td class="px-4 py-4 whitespace-nowrap">
148
+ <span class="text-lg font-bold text-pink-400">{{ "{:,.0f}".format(commission.commission_amount) }} <span class="text-xs font-normal text-gray-400">FCFA</span></span>
149
+ </td>
150
+ <td class="px-4 py-4 whitespace-nowrap">
151
+ <div class="text-sm text-gray-300">{{ commission.created_at.strftime('%d/%m/%Y') }}</div>
152
+ <div class="text-xs text-gray-500">{{ commission.created_at.strftime('%H:%M') }}</div>
153
+ </td>
154
+ </tr>
155
+ {% else %}
156
+ <tr>
157
+ <td colspan="8" class="px-6 py-12 text-center text-gray-500">
158
+ <i class="fas fa-share-nodes text-4xl mb-3"></i>
159
+ <p class="text-lg">Aucune commission de parrainage</p>
160
+ {% if request.args.get('type') %}
161
+ <p class="text-sm mt-1">Aucune commission de type "{{ request.args.get('type') }}"</p>
162
+ <a href="{{ url_for('admin_panel.referral_commissions') }}" class="text-pink-400 hover:text-pink-300 text-sm mt-2 inline-block">
163
+ Voir toutes les commissions
164
+ </a>
165
+ {% endif %}
166
+ </td>
167
+ </tr>
168
+ {% endfor %}
169
+ </tbody>
170
+ </table>
171
+ </div>
172
+ </div>
173
+
174
+ <!-- Pagination -->
175
+ {% if commissions.pages > 1 %}
176
+ <div class="flex items-center justify-between mt-6">
177
+ <div class="text-sm text-gray-400">
178
+ Affichage de {{ commissions.items|length }} sur {{ commissions.total }} commissions
179
+ </div>
180
+ <div class="flex items-center space-x-2">
181
+ {% if commissions.has_prev %}
182
+ <a href="{{ url_for('admin_panel.referral_commissions', page=commissions.prev_num, type=request.args.get('type')) }}"
183
+ class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors">
184
+ <i class="fas fa-chevron-left mr-1"></i> Précédent
185
+ </a>
186
+ {% endif %}
187
+
188
+ <span class="px-4 py-2 bg-gray-800 text-gray-400 rounded-lg">
189
+ Page {{ commissions.page }} / {{ commissions.pages }}
190
+ </span>
191
+
192
+ {% if commissions.has_next %}
193
+ <a href="{{ url_for('admin_panel.referral_commissions', page=commissions.next_num, type=request.args.get('type')) }}"
194
+ class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors">
195
+ Suivant <i class="fas fa-chevron-right ml-1"></i>
196
+ </a>
197
+ {% endif %}
198
+ </div>
199
+ </div>
200
+ {% endif %}
201
+
202
+ <!-- Info Section -->
203
+ <div class="mt-6 bg-gray-800/50 border border-gray-700 rounded-lg p-4">
204
+ <div class="flex items-start">
205
+ <i class="fas fa-info-circle text-pink-400 mt-0.5 mr-3"></i>
206
+ <div class="text-sm text-gray-300">
207
+ <p class="font-medium text-pink-400 mb-2">Système de parrainage</p>
208
+ <ul class="space-y-1 text-gray-400">
209
+ <li>• <strong class="text-yellow-400">15%</strong> de commission sur chaque achat de plan effectué par un filleul</li>
210
+ <li>• <strong class="text-green-400">3%</strong> de commission sur les gains quotidiens des filleuls</li>
211
+ <li>• Les commissions sont <strong class="text-white">cumulables</strong> sans limite de filleuls</li>
212
+ <li>• Les commissions sont créditées <strong class="text-white">automatiquement</strong> sur le solde du parrain</li>
213
+ </ul>
214
+ </div>
215
+ </div>
216
+ </div>
217
+ {% endblock %}
app/templates/admin/settings.html CHANGED
@@ -12,7 +12,7 @@
12
  Configuration de l'Application
13
  </h1>
14
  <p class="text-gray-400">
15
- Modifiez dynamiquement le nom et le logo de votre application sans toucher au code.
16
  </p>
17
  </div>
18
 
@@ -20,11 +20,72 @@
20
  <form method="POST" enctype="multipart/form-data" class="space-y-8">
21
  <input type="hidden" name="csrf_token" value="{{ csrf_token() if csrf_token is defined else '' }}">
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  <!-- App Name Section -->
24
  <div class="bg-gray-800 rounded-xl border border-gray-700 p-6">
25
  <div class="flex items-start space-x-4">
26
- <div class="w-12 h-12 bg-blue-500/20 rounded-lg flex items-center justify-center flex-shrink-0">
27
- <i class="fas fa-font text-blue-400 text-xl"></i>
28
  </div>
29
  <div class="flex-1">
30
  <h3 class="text-lg font-semibold text-white mb-1">Nom de l'Application</h3>
@@ -52,8 +113,8 @@
52
  <!-- App Logo Section -->
53
  <div class="bg-gray-800 rounded-xl border border-gray-700 p-6">
54
  <div class="flex items-start space-x-4">
55
- <div class="w-12 h-12 bg-purple-500/20 rounded-lg flex items-center justify-center flex-shrink-0">
56
- <i class="fas fa-image text-purple-400 text-xl"></i>
57
  </div>
58
  <div class="flex-1">
59
  <h3 class="text-lg font-semibold text-white mb-1">Logo de l'Application</h3>
@@ -71,7 +132,7 @@
71
  </div>
72
  <div class="text-sm text-gray-400">
73
  <p>Logo personnalisé</p>
74
- <button</p> type="submit" name="remove_logo" value="1" class="text-red-400 hover:text-red-300 text-xs mt-1">
75
  <i class="fas fa-trash mr-1"></i> Supprimer le logo
76
  </button>
77
  </div>
@@ -82,7 +143,7 @@
82
  <div class="text-sm text-gray-400">
83
  <p>Logo par défaut</p>
84
  <p class="text-xs">Aucun logo personnalisé</p>
85
- </div>
86
  {% endif %}
87
  </div>
88
  </div>
@@ -218,6 +279,17 @@
218
  fileNameDisplay.classList.add('hidden');
219
  }
220
  });
 
 
 
 
 
 
 
 
 
 
 
221
  </script>
222
  {% endblock %}
223
  {% endblock %}
 
12
  Configuration de l'Application
13
  </h1>
14
  <p class="text-gray-400">
15
+ Modifiez dynamiquement le nom, le logo et les paramètres d'affichage de votre application.
16
  </p>
17
  </div>
18
 
 
20
  <form method="POST" enctype="multipart/form-data" class="space-y-8">
21
  <input type="hidden" name="csrf_token" value="{{ csrf_token() if csrf_token is defined else '' }}">
22
 
23
+ <!-- User Counter Section -->
24
+ <div class="bg-gradient-to-r from-blue-500/10 to-purple-500/10 rounded-xl border border-blue-500/30 p-6">
25
+ <div class="flex items-start space-x-4">
26
+ <div class="w-12 h-12 bg-blue-500/20 rounded-lg flex items-center justify-center flex-shrink-0">
27
+ <i class="fas fa-users text-blue-400 text-xl"></i>
28
+ </div>
29
+ <div class="flex-1">
30
+ <h3 class="text-lg font-semibold text-white mb-1">Compteur d'Utilisateurs (Landing Page)</h3>
31
+ <p class="text-sm text-gray-400 mb-4">
32
+ Configurez le nombre d'utilisateurs affiché sur la page d'accueil. Ce nombre sera additionné aux vraies inscriptions.
33
+ </p>
34
+
35
+ <!-- Current Stats -->
36
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
37
+ <div class="bg-gray-800 rounded-lg p-4 border border-gray-700">
38
+ <p class="text-xs text-gray-400 uppercase tracking-wider mb-1">Utilisateurs Réels</p>
39
+ <p class="text-2xl font-bold text-green-400">{{ real_user_count }}</p>
40
+ </div>
41
+ <div class="bg-gray-800 rounded-lg p-4 border border-gray-700">
42
+ <p class="text-xs text-gray-400 uppercase tracking-wider mb-1">Utilisateurs Ajoutés</p>
43
+ <p class="text-2xl font-bold text-yellow-400">{{ current_fake_user_count }}</p>
44
+ </div>
45
+ <div class="bg-gray-800 rounded-lg p-4 border border-gray-700">
46
+ <p class="text-xs text-gray-400 uppercase tracking-wider mb-1">Total Affiché</p>
47
+ <p class="text-2xl font-bold text-blue-400">{{ displayed_user_count }}</p>
48
+ </div>
49
+ </div>
50
+
51
+ <div class="max-w-md">
52
+ <label for="fake_user_count" class="block text-sm font-medium text-gray-300 mb-2">
53
+ Nombre d'utilisateurs à ajouter
54
+ </label>
55
+ <div class="relative">
56
+ <input type="number"
57
+ id="fake_user_count"
58
+ name="fake_user_count"
59
+ value="{{ current_fake_user_count }}"
60
+ min="0"
61
+ placeholder="Ex: 10000"
62
+ class="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors">
63
+ <div class="absolute inset-y-0 right-0 flex items-center pr-3">
64
+ <i class="fas fa-plus text-gray-400"></i>
65
+ </div>
66
+ </div>
67
+ <p class="mt-2 text-xs text-gray-500">
68
+ Ce nombre sera ajouté aux {{ real_user_count }} utilisateurs réels pour l'affichage public.
69
+ </p>
70
+ </div>
71
+
72
+ <!-- Preview -->
73
+ <div class="mt-4 p-4 bg-gray-900/50 rounded-lg border border-gray-700">
74
+ <p class="text-xs text-gray-400 mb-2">Aperçu sur la landing page:</p>
75
+ <div class="flex items-center space-x-2">
76
+ <span class="text-3xl font-bold text-yellow-400" id="preview_user_count">{{ displayed_user_count }}</span>
77
+ <span class="text-gray-400">Investisseurs</span>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
  <!-- App Name Section -->
85
  <div class="bg-gray-800 rounded-xl border border-gray-700 p-6">
86
  <div class="flex items-start space-x-4">
87
+ <div class="w-12 h-12 bg-purple-500/20 rounded-lg flex items-center justify-center flex-shrink-0">
88
+ <i class="fas fa-font text-purple-400 text-xl"></i>
89
  </div>
90
  <div class="flex-1">
91
  <h3 class="text-lg font-semibold text-white mb-1">Nom de l'Application</h3>
 
113
  <!-- App Logo Section -->
114
  <div class="bg-gray-800 rounded-xl border border-gray-700 p-6">
115
  <div class="flex items-start space-x-4">
116
+ <div class="w-12 h-12 bg-green-500/20 rounded-lg flex items-center justify-center flex-shrink-0">
117
+ <i class="fas fa-image text-green-400 text-xl"></i>
118
  </div>
119
  <div class="flex-1">
120
  <h3 class="text-lg font-semibold text-white mb-1">Logo de l'Application</h3>
 
132
  </div>
133
  <div class="text-sm text-gray-400">
134
  <p>Logo personnalisé</p>
135
+ <button type="submit" name="remove_logo" value="1" class="text-red-400 hover:text-red-300 text-xs mt-1">
136
  <i class="fas fa-trash mr-1"></i> Supprimer le logo
137
  </button>
138
  </div>
 
143
  <div class="text-sm text-gray-400">
144
  <p>Logo par défaut</p>
145
  <p class="text-xs">Aucun logo personnalisé</p>
146
+ </div></p>
147
  {% endif %}
148
  </div>
149
  </div>
 
279
  fileNameDisplay.classList.add('hidden');
280
  }
281
  });
282
+
283
+ // Live preview for user count
284
+ const fakeUserCountInput = document.getElementById('fake_user_count');
285
+ const previewUserCount = document.getElementById('preview_user_count');
286
+ const realUserCount = {{ real_user_count }};
287
+
288
+ fakeUserCountInput.addEventListener('input', function() {
289
+ const fakeCount = parseInt(this.value) || 0;
290
+ const total = realUserCount + fakeCount;
291
+ previewUserCount.textContent = total.toLocaleString('fr-FR');
292
+ });
293
  </script>
294
  {% endblock %}
295
  {% endblock %}
app/templates/admin/withdrawals.html CHANGED
@@ -7,38 +7,48 @@
7
  <!-- Header -->
8
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
9
  <div>
10
- <h1 class="</div>text-2xl font-bold text-white">
11
  <i class="fas fa-money-bill-wave mr-3 text-yellow-400"></i>
12
  Demandes de Retrait
13
  </h1>
14
  <p class="text-gray-400 text-sm mt-1">
15
- Gérez les demandes de retrait des utilisateurs
16
  </p>
17
  </div>
18
 
19
- <!-- Filter Buttons -->
20
- <div class="flex items-center space-x-2">
21
- <a href="{{ url_for('admin_panel.withdrawals') }}"
22
- class="px-4 py-2 rounded-lg font-medium transition-colors {% if not request.args.get('status') %}bg-yellow-600 text-white{% else %}bg-gray-700 text-gray-300 hover:bg-gray-600{% endif %}">
23
- Tous
24
- </a>
25
- <a href="{{ url_for('admin_panel.withdrawals', status='pending') }}"
26
- class="px-4 py-2 rounded-lg font-medium transition-colors {% if request.args.get('status') == 'pending' %}bg-yellow-600 text-white{% else %}bg-gray-700 text-gray-300 hover:bg-gray-600{% endif %}">
27
- <i class="fas fa-clock mr-1"></i> En attente
28
- </a>
29
- <a href="{{ url_for('admin_panel.withdrawals', status='approved') }}"
30
- class="px-4 py-2 rounded-lg font-medium transition-colors {% if request.args.get('status') == 'approved' %}bg-green-600 text-white{% else %}bg-gray-700 text-gray-300 hover:bg-gray-600{% endif %}">
31
- <i class="fas fa-check mr-1"></i> Approuvés
32
- </a>
33
- <a href="{{ url_for('admin_panel.withdrawals', status='rejected') }}"
34
- class="px-4 py-2 rounded-lg font-medium transition-colors {% if request.args.get('status') == 'rejected' %}bg-red-600 text-white{% else %}bg-gray-700 text-gray-300 hover:bg-gray-600{% endif %}">
35
- <i class="fas fa-times mr-1"></i> Rejetés
36
  </a>
37
  </div>
38
  </div>
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  <!-- Stats Cards -->
41
- <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
42
  <div class="bg-gray-800 rounded-lg border border-gray-700 p-4">
43
  <div class="flex items-center justify-between">
44
  <div>
@@ -75,14 +85,41 @@
75
  <div class="bg-gray-800 rounded-lg border border-gray-700 p-4">
76
  <div class="flex items-center justify-between">
77
  <div>
78
- <p class="text-gray-400 text-sm">Total Demandé</p>
79
- <p class="text-xl font-bold text-white">{{ "{:,.0f}".format(total_pending_amount) }} <span class="text-sm text-gray-400">FCFA</span></p>
80
  </div>
81
  <div class="w-10 h-10 bg-purple-500/20 rounded-lg flex items-center justify-center">
82
  <i class="fas fa-wallet text-purple-400"></i>
83
  </div>
84
  </div>
85
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  </div>
87
 
88
  <!-- Withdrawals Table -->
@@ -91,22 +128,24 @@
91
  <table class="w-full">
92
  <thead class="bg-gray-900/50">
93
  <tr>
94
- <th class="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">ID</th>
95
- <th class="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Utilisateur</th>
96
- <th class="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Montant</th>
97
- <th class="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Destination</th>
98
- <th class="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Statut</th>
99
- <th class="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Date</th>
100
- <th class="px-6 py-4 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">Actions</th>
 
 
101
  </tr>
102
  </thead>
103
  <tbody class="divide-y divide-gray-700">
104
  {% for withdrawal in withdrawals %}
105
  <tr class="hover:bg-gray-700/50 transition-colors">
106
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
107
  #{{ withdrawal.id }}
108
  </td>
109
- <td class="px-6 py-4 whitespace-nowrap">
110
  <div class="flex items-center">
111
  <div class="w-10 h-10 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-full flex items-center justify-center">
112
  <span class="text-white font-bold text-sm">{{ withdrawal.user.name[0].upper() if withdrawal.user and withdrawal.user.name else '?' }}</span>
@@ -117,16 +156,35 @@
117
  </div>
118
  </div>
119
  </td>
120
- <td class="px-6 py-4 whitespace-nowrap">
121
- <span class="text-xl font-bold text-red-400">{{ "{:,.0f}".format(withdrawal.amount) }} <span class="text-sm font-normal">FCFA</span></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  </td>
123
- <td class="px-6 py-4 whitespace-nowrap">
124
  <div class="flex items-center">
125
  <i class="fas fa-mobile-alt text-gray-400 mr-2"></i>
126
- <span class="text-gray-300">{{ withdrawal.description.replace('Retrait vers ', '') if withdrawal.description else 'N/A' }}</span>
127
  </div>
128
  </td>
129
- <td class="px-6 py-4 whitespace-nowrap">
130
  {% if withdrawal.status == 'pending' %}
131
  <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-yellow-500/20 text-yellow-400 border border-yellow-500/30">
132
  <i class="fas fa-clock mr-1.5"></i> En attente
@@ -134,6 +192,9 @@
134
  {% elif withdrawal.status == 'approved' %}
135
  <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-500/20 text-green-400 border border-green-500/30">
136
  <i class="fas fa-check mr-1.5"></i> Approuvé
 
 
 
137
  </span>
138
  {% elif withdrawal.status == 'rejected' %}
139
  <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-red-500/20 text-red-400 border border-red-500/30">
@@ -149,40 +210,73 @@
149
  </span>
150
  {% endif %}
151
  </td>
152
- <td class="px-6 py-4 whitespace-nowrap">
153
- <div class="text-sm text-gray-300">{{ withdrawal.created_at.strftime('%d/%m/%Y') }}</div>
154
- <div class="text-xs text-gray-500">{{ withdrawal.created_at.strftime('%H:%M') }}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  </td>
156
- <td class="px-6 py-4 whitespace-nowrap">
157
  {% if withdrawal.status == 'pending' %}
158
- <div class="flex items-center justify-center space-x-2">
159
  <!-- Approve Button -->
160
  <a href="{{ url_for('admin_panel.process_withdrawal', transaction_id=withdrawal.id, action='approve') }}"
161
- onclick="return confirm('Êtes-vous sûr de vouloir approuver ce retrait de {{ '{:,.0f}'.format(withdrawal.amount) }} FCFA ?')"
162
- class="inline-flex items-center px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-lg transition-colors">
163
  <i class="fas fa-check mr-1.5"></i> Approuver
164
  </a>
165
  <!-- Reject Button -->
166
  <a href="{{ url_for('admin_panel.process_withdrawal', transaction_id=withdrawal.id, action='reject') }}"
167
- onclick="return confirm('Êtes-vous sûr de vouloir rejeter ce retrait ? Le montant sera remboursé à l\'utilisateur.')"
168
- class="inline-flex items-center px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-lg transition-colors">
169
  <i class="fas fa-times mr-1.5"></i> Rejeter
170
  </a>
171
  </div>
172
  {% else %}
173
- <span class="text-gray-500 text-sm">
174
- {% if withdrawal.processed_at %}
175
- Traité le {{ withdrawal.processed_at.strftime('%d/%m/%Y') }}
176
- {% else %}
177
- -
178
- {% endif %}
179
- </span>
 
 
 
 
 
 
 
 
 
180
  {% endif %}
181
  </td>
182
  </tr>
183
  {% else %}
184
  <tr>
185
- <td colspan="7" class="px-6 py-12 text-center text-gray-500">
186
  <i class="fas fa-inbox text-4xl mb-3"></i>
187
  <p class="text-lg">Aucune demande de retrait</p>
188
  {% if request.args.get('status') %}
@@ -199,16 +293,45 @@
199
  </div>
200
  </div>
201
 
202
- <!-- Info Section -->
203
- <div class="mt-6 bg-blue-900/30 border border-blue-700/50 rounded-lg p-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  <div class="flex items-start">
205
- <i class="fas fa-info-circle text-blue-400 mt-0.5 mr-3"></i>
206
  <div class="text-sm text-gray-300">
207
- <p class="font-medium text-blue-400 mb-1">Instructions de traitement</p>
208
  <ul class="space-y-1 text-gray-400">
209
- <li>• <strong>Approuver</strong> : Marque le retrait comme approuvé. Vous devez ensuite effectuer le virement manuellement.</li>
210
- <li>• <strong>Rejeter</strong> : Le retrait est annulé et le montant est automatiquement remboursé sur le solde de l'utilisateur.</li>
211
- <li>• Les retraits sont traités dans l'ordre d'arrivée (FIFO).</li>
 
212
  </ul>
213
  </div>
214
  </div>
 
7
  <!-- Header -->
8
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
9
  <div>
10
+ <h1 class="text-2xl font-bold text-white">
11
  <i class="fas fa-money-bill-wave mr-3 text-yellow-400"></i>
12
  Demandes de Retrait
13
  </h1>
14
  <p class="text-gray-400 text-sm mt-1">
15
+ Gérez les demandes de retrait des utilisateurs (frais de 15% appliqués)
16
  </p>
17
  </div>
18
 
19
+ <div class="flex items-center space-x-3">
20
+ <!-- Auto Process Button -->
21
+ <a href="{{ url_for('admin_panel.auto_process_withdrawals') }}"
22
+ onclick="return confirm('Traiter automatiquement tous les retraits qui ont dépassé le délai de 24h ?')"
23
+ class="inline-flex items-center px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors">
24
+ <i class="fas fa-robot mr-2"></i>
25
+ Traitement Auto 24h
 
 
 
 
 
 
 
 
 
 
26
  </a>
27
  </div>
28
  </div>
29
 
30
+ <!-- Filter Buttons -->
31
+ <div class="flex flex-wrap items-center gap-2 mb-6">
32
+ <a href="{{ url_for('admin_panel.withdrawals') }}"
33
+ class="px-4 py-2 rounded-lg font-medium transition-colors {% if not request.args.get('status') %}bg-yellow-600 text-white{% else %}bg-gray-700 text-gray-300 hover:bg-gray-600{% endif %}">
34
+ Tous
35
+ </a>
36
+ <a href="{{ url_for('admin_panel.withdrawals', status='pending') }}"
37
+ class="px-4 py-2 rounded-lg font-medium transition-colors {% if request.args.get('status') == 'pending' %}bg-yellow-600 text-white{% else %}bg-gray-700 text-gray-300 hover:bg-gray-600{% endif %}">
38
+ <i class="fas fa-clock mr-1"></i> En attente
39
+ </a>
40
+ <a href="{{ url_for('admin_panel.withdrawals', status='approved') }}"
41
+ class="px-4 py-2 rounded-lg font-medium transition-colors {% if request.args.get('status') == 'approved' %}bg-green-600 text-white{% else %}bg-gray-700 text-gray-300 hover:bg-gray-600{% endif %}">
42
+ <i class="fas fa-check mr-1"></i> Approuvés
43
+ </a>
44
+ <a href="{{ url_for('admin_panel.withdrawals', status='rejected') }}"
45
+ class="px-4 py-2 rounded-lg font-medium transition-colors {% if request.args.get('status') == 'rejected' %}bg-red-600 text-white{% else %}bg-gray-700 text-gray-300 hover:bg-gray-600{% endif %}">
46
+ <i class="fas fa-times mr-1"></i> Rejetés
47
+ </a>
48
+ </div>
49
+
50
  <!-- Stats Cards -->
51
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
52
  <div class="bg-gray-800 rounded-lg border border-gray-700 p-4">
53
  <div class="flex items-center justify-between">
54
  <div>
 
85
  <div class="bg-gray-800 rounded-lg border border-gray-700 p-4">
86
  <div class="flex items-center justify-between">
87
  <div>
88
+ <p class="text-gray-400 text-sm">Montant en Attente</p>
89
+ <p class="text-lg font-bold text-white">{{ "{:,.0f}".format(total_pending_amount) }} <span class="text-xs text-gray-400">FCFA</span></p>
90
  </div>
91
  <div class="w-10 h-10 bg-purple-500/20 rounded-lg flex items-center justify-center">
92
  <i class="fas fa-wallet text-purple-400"></i>
93
  </div>
94
  </div>
95
  </div>
96
+ <div class="bg-gradient-to-br from-yellow-500/20 to-amber-500/20 rounded-lg border border-yellow-500/30 p-4">
97
+ <div class="flex items-center justify-between">
98
+ <div>
99
+ <p class="text-gray-400 text-sm">Frais Collectés (15%)</p>
100
+ <p class="text-lg font-bold text-yellow-400">{{ "{:,.0f}".format(total_fees_collected) }} <span class="text-xs text-gray-400">FCFA</span></p>
101
+ </div>
102
+ <div class="w-10 h-10 bg-yellow-500/20 rounded-lg flex items-center justify-center">
103
+ <i class="fas fa-coins text-yellow-400"></i>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <!-- Info Banner for 24h delay -->
110
+ <div class="mb-6 bg-blue-900/30 border border-blue-700/50 rounded-lg p-4">
111
+ <div class="flex items-start">
112
+ <i class="fas fa-hourglass-half text-blue-400 mt-0.5 mr-3"></i>
113
+ <div class="text-sm text-gray-300">
114
+ <p class="font-medium text-blue-400 mb-1">Politique de retrait - Délai 24h</p>
115
+ <ul class="space-y-1 text-gray-400">
116
+ <li>• Les retraits sont mis en attente pendant <strong class="text-white">24 heures</strong> pour permettre la vérification.</li>
117
+ <li>• Si aucune action n'est prise, le retrait est <strong class="text-green-400">automatiquement approuvé</strong> après 24h.</li>
118
+ <li>• Les retraits du vendredi sont traités le <strong class="text-yellow-400">lundi</strong> (jours ouvrables uniquement).</li>
119
+ <li>• Des frais de <strong class="text-yellow-400">15%</strong> sont appliqués sur chaque retrait.</li>
120
+ </ul>
121
+ </div>
122
+ </div>
123
  </div>
124
 
125
  <!-- Withdrawals Table -->
 
128
  <table class="w-full">
129
  <thead class="bg-gray-900/50">
130
  <tr>
131
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">ID</th>
132
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Utilisateur</th>
133
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Montant Brut</th>
134
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Frais (15%)</th>
135
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Montant Net</th>
136
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Destination</th>
137
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Statut</th>
138
+ <th class="px-4 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Traitement Prévu</th>
139
+ <th class="px-4 py-4 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">Actions</th>
140
  </tr>
141
  </thead>
142
  <tbody class="divide-y divide-gray-700">
143
  {% for withdrawal in withdrawals %}
144
  <tr class="hover:bg-gray-700/50 transition-colors">
145
+ <td class="px-4 py-4 whitespace-nowrap text-sm text-gray-400">
146
  #{{ withdrawal.id }}
147
  </td>
148
+ <td class="px-4 py-4 whitespace-nowrap">
149
  <div class="flex items-center">
150
  <div class="w-10 h-10 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-full flex items-center justify-center">
151
  <span class="text-white font-bold text-sm">{{ withdrawal.user.name[0].upper() if withdrawal.user and withdrawal.user.name else '?' }}</span>
 
156
  </div>
157
  </div>
158
  </td>
159
+ <td class="px-4 py-4 whitespace-nowrap">
160
+ <span class="text-lg font-bold text-white">{{ "{:,.0f}".format(withdrawal.amount) }}</span>
161
+ <span class="text-xs text-gray-400">FCFA</span>
162
+ </td>
163
+ <td class="px-4 py-4 whitespace-nowrap">
164
+ {% if withdrawal.fee_amount %}
165
+ <span class="text-lg font-bold text-yellow-400">{{ "{:,.0f}".format(withdrawal.fee_amount) }}</span>
166
+ <span class="text-xs text-gray-400">FCFA</span>
167
+ {% else %}
168
+ <span class="text-lg font-bold text-yellow-400">{{ "{:,.0f}".format(withdrawal.amount * 0.15) }}</span>
169
+ <span class="text-xs text-gray-400">FCFA</span>
170
+ {% endif %}
171
+ </td>
172
+ <td class="px-4 py-4 whitespace-nowrap">
173
+ {% if withdrawal.net_amount %}
174
+ <span class="text-lg font-bold text-green-400">{{ "{:,.0f}".format(withdrawal.net_amount) }}</span>
175
+ <span class="text-xs text-gray-400">FCFA</span>
176
+ {% else %}
177
+ <span class="text-lg font-bold text-green-400">{{ "{:,.0f}".format(withdrawal.amount * 0.85) }}</span>
178
+ <span class="text-xs text-gray-400">FCFA</span>
179
+ {% endif %}
180
  </td>
181
+ <td class="px-4 py-4 whitespace-nowrap">
182
  <div class="flex items-center">
183
  <i class="fas fa-mobile-alt text-gray-400 mr-2"></i>
184
+ <span class="text-gray-300 text-sm">{{ withdrawal.description.replace('Retrait vers ', '') if withdrawal.description else 'N/A' }}</span>
185
  </div>
186
  </td>
187
+ <td class="px-4 py-4 whitespace-nowrap">
188
  {% if withdrawal.status == 'pending' %}
189
  <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-yellow-500/20 text-yellow-400 border border-yellow-500/30">
190
  <i class="fas fa-clock mr-1.5"></i> En attente
 
192
  {% elif withdrawal.status == 'approved' %}
193
  <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-500/20 text-green-400 border border-green-500/30">
194
  <i class="fas fa-check mr-1.5"></i> Approuvé
195
+ {% if withdrawal.admin_action == 'auto_approved' %}
196
+ <span class="ml-1 text-[10px] text-gray-400">(auto)</span>
197
+ {% endif %}
198
  </span>
199
  {% elif withdrawal.status == 'rejected' %}
200
  <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-red-500/20 text-red-400 border border-red-500/30">
 
210
  </span>
211
  {% endif %}
212
  </td>
213
+ <td class="px-4 py-4 whitespace-nowrap">
214
+ {% if withdrawal.status == 'pending' %}
215
+ {% if withdrawal.scheduled_process_time %}
216
+ <div class="text-sm">
217
+ <div class="text-gray-300">{{ withdrawal.scheduled_process_time.strftime('%d/%m/%Y') }}</div>
218
+ <div class="text-xs text-gray-500">{{ withdrawal.scheduled_process_time.strftime('%H:%M') }}</div>
219
+ {% set now = None %}
220
+ {% if withdrawal.scheduled_process_time %}
221
+ <div class="text-[10px] mt-1 text-orange-400">
222
+ <i class="fas fa-hourglass-half mr-1"></i>
223
+ En attente de traitement
224
+ </div>
225
+ {% endif %}
226
+ </div>
227
+ {% else %}
228
+ <span class="text-gray-500 text-sm">Non défini</span>
229
+ {% endif %}
230
+ {% else %}
231
+ {% if withdrawal.processed_at %}
232
+ <div class="text-sm text-gray-400">
233
+ <div>{{ withdrawal.processed_at.strftime('%d/%m/%Y') }}</div>
234
+ <div class</div>="text-xs">{{ withdrawal.processed_at.strftime('%H:%M') }}</div>
235
+ </div>
236
+ {% else %}
237
+ <span class="text-gray-500 text-sm">-</span>
238
+ {% endif %}
239
+ {% endif %}
240
  </td>
241
+ <td class="px-4 py-4 whitespace-nowrap">
242
  {% if withdrawal.status == 'pending' %}
243
+ <div class="flex flex-col items-center space-y-2">
244
  <!-- Approve Button -->
245
  <a href="{{ url_for('admin_panel.process_withdrawal', transaction_id=withdrawal.id, action='approve') }}"
246
+ onclick="return confirm('Approuver ce retrait ?\n\nMontant brut: {{ '{:,.0f}'.format(withdrawal.amount) }} FCFA\nFrais (15%): {{ '{:,.0f}'.format(withdrawal.fee_amount or withdrawal.amount * 0.15) }} FCFA\nMontant net à envoyer: {{ '{:,.0f}'.format(withdrawal.net_amount or withdrawal.amount * 0.85) }} FCFA')"
247
+ class="w-full inline-flex items-center justify-center px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-lg transition-colors">
248
  <i class="fas fa-check mr-1.5"></i> Approuver
249
  </a>
250
  <!-- Reject Button -->
251
  <a href="{{ url_for('admin_panel.process_withdrawal', transaction_id=withdrawal.id, action='reject') }}"
252
+ onclick="return confirm('Rejeter ce retrait ?\n\nLe montant de {{ '{:,.0f}'.format(withdrawal.amount) }} FCFA sera remboursé à l\'utilisateur.')"
253
+ class="w-full inline-flex items-center justify-center px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-lg transition-colors">
254
  <i class="fas fa-times mr-1.5"></i> Rejeter
255
  </a>
256
  </div>
257
  {% else %}
258
+ <div class="text-center">
259
+ <span class="text-gray-500 text-sm">
260
+ {% if withdrawal.admin_action == 'auto_approved' %}
261
+ <i class="fas fa-robot text-purple-400 mr-1"></i>
262
+ Auto-traité
263
+ {% elif withdrawal.admin_action == 'approved' %}
264
+ <i class="fas fa-user-check text-green-400 mr-1"></i>
265
+ Approuvé manuellement
266
+ {% elif withdrawal.admin_action == 'rejected' %}
267
+ <i class="fas fa-user-times text-red-400 mr-1"></i>
268
+ Rejeté manuellement
269
+ {% else %}
270
+ Traité
271
+ {% endif %}
272
+ </span>
273
+ </div>
274
  {% endif %}
275
  </td>
276
  </tr>
277
  {% else %}
278
  <tr>
279
+ <td colspan="9" class="px-6 py-12 text-center text-gray-500">
280
  <i class="fas fa-inbox text-4xl mb-3"></i>
281
  <p class="text-lg">Aucune demande de retrait</p>
282
  {% if request.args.get('status') %}
 
293
  </div>
294
  </div>
295
 
296
+ <!-- Summary Card -->
297
+ <div class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4">
298
+ <div class="bg-gray-800 rounded-lg border border-gray-700 p-4">
299
+ <h4 class="text-sm font-medium text-gray-400 mb-2">
300
+ <i class="fas fa-calculator mr-2 text-blue-400"></i>
301
+ Frais en Attente
302
+ </h4>
303
+ <p class="text-2xl font-bold text-yellow-400">{{ "{:,.0f}".format(total_pending_fees) }} <span class="text-sm font-normal text-gray-400">FCFA</span></p>
304
+ <p class="text-xs text-gray-500 mt-1">Frais à collecter sur les retraits en attente</p>
305
+ </div>
306
+ <div class="bg-gray-800 rounded-lg border border-gray-700 p-4">
307
+ <h4 class="text-sm font-medium text-gray-400 mb-2">
308
+ <i class="fas fa-coins mr-2 text-green-400"></i>
309
+ Total Frais Collectés
310
+ </h4>
311
+ <p class="text-2xl font-bold text-green-400">{{ "{:,.0f}".format(total_fees_collected) }} <span class="text-sm font-normal text-gray-400">FCFA</span></p>
312
+ <p class="text-xs text-gray-500 mt-1">15% sur tous les retraits approuvés</p>
313
+ </div>
314
+ <div class="bg-gray-800 rounded-lg border border-gray-700 p-4">
315
+ <h4 class="text-sm font-medium text-gray-400 mb-2">
316
+ <i class="fas fa-percentage mr-2 text-purple-400"></i>
317
+ Taux de Frais
318
+ </h4>
319
+ <p class="text-2xl font-bold text-purple-400">15%</p>
320
+ <p class="text-xs text-gray-500 mt-1">Appliqué automatiquement sur chaque retrait</p>
321
+ </div>
322
+ </div>
323
+
324
+ <!-- Instructions -->
325
+ <div class="mt-6 bg-gray-800/50 border border-gray-700 rounded-lg p-4">
326
  <div class="flex items-start">
327
+ <i class="fas fa-info-circle text-yellow-400 mt-0.5 mr-3"></i>
328
  <div class="text-sm text-gray-300">
329
+ <p class="font-medium text-yellow-400 mb-2">Instructions de traitement</p>
330
  <ul class="space-y-1 text-gray-400">
331
+ <li>• <strong class="text-green-400">Approuver</strong> : Le retrait est validé. Envoyez le <strong>montant net</strong> (après frais 15%) à l'utilisateur.</li>
332
+ <li>• <strong class="text-red-400">Rejeter</strong> : Le retrait est annulé et le <strong>montant brut</strong> est automatiquement remboursé sur le solde.</li>
333
+ <li>• <strong class="text-purple-400">Traitement Auto</strong> : Approuve automatiquement tous les retraits qui ont dépassé le délai de 24h sans action manuelle.</li>
334
+ <li>• Les retraits sont traités uniquement les <strong>jours ouvrables</strong> (lundi au vendredi).</li>
335
  </ul>
336
  </div>
337
  </div>
app/templates/dashboard.html CHANGED
@@ -121,6 +121,41 @@ block content %}
121
  </a>
122
  </div>
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  <!-- LISTE DES PLANS ACTIFS -->
125
  <div>
126
  <div class="flex justify-between items-end mb-4">
@@ -183,6 +218,103 @@ block content %}
183
 
184
  <!-- ==================== MODALS ==================== -->
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  <!-- MODAL DÉPÔT -->
187
  <div
188
  id="deposit-modal"
@@ -256,6 +388,18 @@ block content %}
256
  </p>
257
  </div>
258
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  <a
260
  href="{{ url_for('main.withdraw') }}"
261
  class="w-full btn-press bg-white text-black font-bold py-4 rounded-xl flex items-center justify-center gap-2 hover:bg-gray-200"
@@ -265,10 +409,10 @@ block content %}
265
  </a>
266
 
267
  <div class="flex items-start gap-3 bg-gray-800/50 p-3 rounded-lg">
268
- <i class="fa-solid fa-info-circle text-accent mt-1"></i>
269
  <p class="text-xs text-gray-400 leading-relaxed">
270
- Les retraits sont traités sous 24h. Assurez-vous que vos
271
- informations sont correctes.
272
  </p>
273
  </div>
274
  </div>
@@ -283,5 +427,23 @@ block content %}
283
  function closeModal(id) {
284
  document.getElementById(id).classList.add("hidden");
285
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  </script>
287
  {% endblock %}
 
121
  </a>
122
  </div>
123
 
124
+ <!-- BANNIÈRE PARRAINAGE -->
125
+ <div class="mb-8">
126
+ <a
127
+ href="{{ url_for('main.referral') }}"
128
+ class="block w-full btn-press bg-gradient-to-r from-green-600 to-emerald-500 rounded-2xl p-1 relative overflow-hidden text-left group"
129
+ >
130
+ <div
131
+ class="bg-gray-900/10 absolute inset-0 group-hover:bg-transparent transition"
132
+ ></div>
133
+ <div class="flex items-center justify-between p-4 relative z-10">
134
+ <div class="flex items-center gap-4">
135
+ <div
136
+ class="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center text-white"
137
+ >
138
+ <i class="fa-solid fa-users text-xl"></i>
139
+ </div>
140
+ <div>
141
+ <h3 class="font-bold text-white text-lg">
142
+ Parrainez & Gagnez
143
+ </h3>
144
+ <p class="text-green-100 text-xs">
145
+ {{ purchase_commission_rate }}% sur achats + {{
146
+ daily_gain_commission_rate }}% sur gains quotidiens
147
+ </p>
148
+ </div>
149
+ </div>
150
+ <div
151
+ class="w-8 h-8 bg-white text-green-600 rounded-full flex items-center justify-center"
152
+ >
153
+ <i class="fa-solid fa-chevron-right text-sm"></i>
154
+ </div>
155
+ </div>
156
+ </a>
157
+ </div>
158
+
159
  <!-- LISTE DES PLANS ACTIFS -->
160
  <div>
161
  <div class="flex justify-between items-end mb-4">
 
218
 
219
  <!-- ==================== MODALS ==================== -->
220
 
221
+ <!-- MODAL POPUP DE BIENVENUE (WhatsApp Share) -->
222
+ {% if show_welcome_popup %}
223
+ <div
224
+ id="welcome-popup-modal"
225
+ class="fixed inset-0 bg-black/90 backdrop-blur-sm z-50 flex items-center justify-center transition-all duration-300 p-4"
226
+ >
227
+ <div
228
+ class="bg-card w-full max-w-md rounded-3xl p-6 animate-slide-up border border-gray-700 relative overflow-hidden"
229
+ >
230
+ <!-- Effet de fond -->
231
+ <div
232
+ class="absolute top-0 right-0 w-40 h-40 bg-green-500/10 rounded-full blur-3xl -mr-20 -mt-20"
233
+ ></div>
234
+ <div
235
+ class="absolute bottom-0 left-0 w-40 h-40 bg-yellow-500/10 rounded-full blur-3xl -ml-20 -mb-20"
236
+ ></div>
237
+
238
+ <div class="relative z-10 text-center space-y-6">
239
+ <!-- Close Button -->
240
+ <button
241
+ onclick="dismissWelcomePopup()"
242
+ class="absolute top-0 right-0 w-8 h-8 bg-gray-800 hover:bg-gray-700 rounded-full flex items-center justify-center text-gray-400 hover:text-white transition"
243
+ >
244
+ <i class="fa-solid fa-times"></i>
245
+ </button>
246
+
247
+ <!-- Icon -->
248
+ <div
249
+ class="w-20 h-20 bg-gradient-to-br from-green-500 to-emerald-600 rounded-full flex items-center justify-center mx-auto animate-bounce"
250
+ >
251
+ <i class="fa-brands fa-whatsapp text-white text-4xl"></i>
252
+ </div>
253
+
254
+ <!-- Title -->
255
+ <div>
256
+ <h3 class="text-2xl font-bold text-white mb-2">
257
+ 🎉 Gagnez encore plus !
258
+ </h3>
259
+ <p class="text-gray-400 text-sm leading-relaxed">
260
+ Partagez votre lien de parrainage sur WhatsApp et gagnez
261
+ <span class="text-yellow-400 font-bold"
262
+ >{{ purchase_commission_rate }}%</span
263
+ >
264
+ sur chaque achat de vos filleuls +
265
+ <span class="text-green-400 font-bold"
266
+ >{{ daily_gain_commission_rate }}%</span
267
+ >
268
+ sur leurs gains quotidiens !
269
+ </p>
270
+ </div>
271
+
272
+ <!-- Referral Code Display -->
273
+ <div class="bg-gray-800/80 rounded-2xl p-4 border border-gray-700">
274
+ <p
275
+ class="text-[10px] text-gray-400 uppercase tracking-wider mb-2"
276
+ >
277
+ Votre code de parrainage
278
+ </p>
279
+ <p class="text-2xl font-bold text-yellow-400 tracking-widest">
280
+ {{ current_user.referral_code }}
281
+ </p>
282
+ </div>
283
+
284
+ <!-- WhatsApp Share Button -->
285
+ <a
286
+ href="https://wa.me/?text=J'ai%20d%C3%A9j%C3%A0%20gagn%C3%A9%201000f%20en%20suivant%20ce%20lien.%20Gagnez%20de%20l'argent%20chaque%20jour%20gr%C3%A2ce%20aux%20m%C3%A9taux%20rares.%20%F0%9F%92%B0%0A%0AInscrivez-vous%20ici%20%3A%20{{ request.url_root }}auth/register?ref={{ current_user.referral_code }}"
287
+ target="_blank"
288
+ class="w-full btn-press bg-green-500 hover:bg-green-600 text-white font-bold py-4 rounded-xl shadow-lg flex items-center justify-center gap-3 transition"
289
+ >
290
+ <i class="fa-brands fa-whatsapp text-2xl"></i>
291
+ <span>Partager sur WhatsApp</span>
292
+ </a>
293
+
294
+ <!-- Skip Button -->
295
+ <button
296
+ onclick="dismissWelcomePopup()"
297
+ class="text-gray-500 hover:text-gray-400 text-sm transition"
298
+ >
299
+ Peut-être plus tard
300
+ </button>
301
+
302
+ <!-- Info Text -->
303
+ <div
304
+ class="flex items-start gap-3 bg-yellow-500/10 p-3 rounded-xl border border-yellow-500/20 text-left"
305
+ >
306
+ <i class="fa-solid fa-lightbulb text-yellow-400 mt-0.5"></i>
307
+ <p class="text-[11px] text-gray-400 leading-relaxed">
308
+ <strong class="text-yellow-400">Astuce :</strong> Plus vous
309
+ parrainez, plus vous gagnez ! Les commissions sont
310
+ cumulables sans limite.
311
+ </p>
312
+ </div>
313
+ </div>
314
+ </div>
315
+ </div>
316
+ {% endif %}
317
+
318
  <!-- MODAL DÉPÔT -->
319
  <div
320
  id="deposit-modal"
 
388
  </p>
389
  </div>
390
 
391
+ <!-- Info frais -->
392
+ <div
393
+ class="bg-yellow-500/10 rounded-xl p-3 border border-yellow-500/20"
394
+ >
395
+ <div class="flex items-center gap-2 text-yellow-400">
396
+ <i class="fa-solid fa-info-circle"></i>
397
+ <span class="text-xs font-medium"
398
+ >Frais de retrait : {{ withdrawal_fee_rate }}%</span
399
+ >
400
+ </div>
401
+ </div>
402
+
403
  <a
404
  href="{{ url_for('main.withdraw') }}"
405
  class="w-full btn-press bg-white text-black font-bold py-4 rounded-xl flex items-center justify-center gap-2 hover:bg-gray-200"
 
409
  </a>
410
 
411
  <div class="flex items-start gap-3 bg-gray-800/50 p-3 rounded-lg">
412
+ <i class="fa-solid fa-clock text-accent mt-1"></i>
413
  <p class="text-xs text-gray-400 leading-relaxed">
414
+ Les retraits sont mis en attente pendant 24h puis traités
415
+ les jours ouvrables (lundi au vendredi).
416
  </p>
417
  </div>
418
  </div>
 
427
  function closeModal(id) {
428
  document.getElementById(id).classList.add("hidden");
429
  }
430
+
431
+ // Dismiss welcome popup and mark as seen
432
+ function dismissWelcomePopup() {
433
+ const popup = document.getElementById("welcome-popup-modal");
434
+ if (popup) {
435
+ popup.style.opacity = "0";
436
+ setTimeout(() => {
437
+ popup.remove();
438
+ }, 300);
439
+
440
+ // Call API to mark popup as seen
441
+ fetch('{{ url_for("main.dismiss_welcome_popup") }}')
442
+ .then((response) => response.json())
443
+ .catch((error) =>
444
+ console.log("Error dismissing popup:", error),
445
+ );
446
+ }
447
+ }
448
  </script>
449
  {% endblock %}
app/templates/index.html CHANGED
@@ -1,5 +1,5 @@
1
- {% extends "base.html" %} {% block title %}Apex Ores - Achetez en Métaux Précieux{%
2
- endblock %} {% block content %}
3
  <section
4
  class="relative min-h-screen flex items-center justify-center overflow-hidden"
5
  >
@@ -59,12 +59,32 @@ endblock %} {% block content %}
59
  </p>
60
  </div>
61
 
 
62
  <div class="grid grid-cols-3 gap-6 py-6">
63
- <div class="text-center">
64
- <div class="text-3xl font-bold text-yellow-400">
65
- 10K+
 
 
 
 
 
 
 
 
66
  </div>
67
  <div class="text-sm text-gray-400">Investisseurs</div>
 
 
 
 
 
 
 
 
 
 
 
68
  </div>
69
  <div class="text-center">
70
  <div class="text-3xl font-bold text-green-400">98%</div>
@@ -279,20 +299,21 @@ endblock %} {% block content %}
279
  </h3>
280
  <div class="space-y-3">
281
  <div class="flex items-center space-x-3">
282
- <i class="fas fa-layer-group text-blue-400"></i>
283
  <span class="text-gray-300"
284
- >Commissions sur
285
- <span class="text-blue-400 font-bold"
286
- >3 niveaux</span
287
- ></span
288
  >
289
  </div>
290
  <div class="flex items-center space-x-3">
291
  <i class="fas fa-percentage text-blue-400"></i>
292
  <span class="text-gray-300"
293
- >Jusqu'à
294
- <span class="text-blue-400 font-bold">25%</span> par
295
- parrainage</span
 
296
  >
297
  </div>
298
  </div>
@@ -440,7 +461,7 @@ endblock %} {% block content %}
440
  >
441
  </a>
442
 
443
- <div class="mt-8 flex items-center justify-center space-x-8">
444
  <div class="flex items-center space-x-2 text-gray-400">
445
  <i class="fas fa-check-circle text-green-400"></i>
446
  <span>Inscription gratuite</span>
@@ -457,4 +478,57 @@ endblock %} {% block content %}
457
  </div>
458
  </div>
459
  </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  {% endblock %}
 
1
+ {% extends "base.html" %} {% block title %}Apex Ores - Achetez en Métaux
2
+ Précieux{% endblock %} {% block content %}
3
  <section
4
  class="relative min-h-screen flex items-center justify-center overflow-hidden"
5
  >
 
59
  </p>
60
  </div>
61
 
62
+ <!-- Animated User Counter Section -->
63
  <div class="grid grid-cols-3 gap-6 py-6">
64
+ <div class="text-center relative">
65
+ <div class="flex items-center justify-center space-x-1">
66
+ <span
67
+ id="user-counter"
68
+ class="text-3xl font-bold text-yellow-400"
69
+ data-target="{{ displayed_user_count }}"
70
+ >0</span
71
+ >
72
+ <span class="text-xl font-bold text-yellow-400"
73
+ >+</span
74
+ >
75
  </div>
76
  <div class="text-sm text-gray-400">Investisseurs</div>
77
+ <!-- Live indicator -->
78
+ <div class="absolute -top-2 -right-2 flex items-center">
79
+ <span class="relative flex h-2 w-2">
80
+ <span
81
+ class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
82
+ ></span>
83
+ <span
84
+ class="relative inline-flex rounded-full h-2 w-2 bg-green-400"
85
+ ></span>
86
+ </span>
87
+ </div>
88
  </div>
89
  <div class="text-center">
90
  <div class="text-3xl font-bold text-green-400">98%</div>
 
299
  </h3>
300
  <div class="space-y-3">
301
  <div class="flex items-center space-x-3">
302
+ <i class="fas fa-hand-holding-usd text-blue-400"></i>
303
  <span class="text-gray-300"
304
+ ><span class="text-blue-400 font-bold"
305
+ >{{ purchase_commission_rate }}%</span
306
+ >
307
+ sur les achats de filleuls</span
308
  >
309
  </div>
310
  <div class="flex items-center space-x-3">
311
  <i class="fas fa-percentage text-blue-400"></i>
312
  <span class="text-gray-300"
313
+ ><span class="text-blue-400 font-bold"
314
+ >{{ daily_gain_commission_rate }}%</span
315
+ >
316
+ sur leurs gains quotidiens</span
317
  >
318
  </div>
319
  </div>
 
461
  >
462
  </a>
463
 
464
+ <div class="mt-8 flex flex-wrap items-center justify-center gap-6">
465
  <div class="flex items-center space-x-2 text-gray-400">
466
  <i class="fas fa-check-circle text-green-400"></i>
467
  <span>Inscription gratuite</span>
 
478
  </div>
479
  </div>
480
  </section>
481
+ {% endblock %} {% block extra_js %}
482
+ <script>
483
+ // Animated counter function
484
+ function animateCounter(element, target, duration = 2000) {
485
+ let start = 0;
486
+ const increment = target / (duration / 16);
487
+
488
+ function updateCounter() {
489
+ start += increment;
490
+ if (start < target) {
491
+ element.textContent = Math.floor(start).toLocaleString("fr-FR");
492
+ requestAnimationFrame(updateCounter);
493
+ } else {
494
+ element.textContent = target.toLocaleString("fr-FR");
495
+ }
496
+ }
497
+
498
+ updateCounter();
499
+ }
500
+
501
+ // Initialize counter when page loads
502
+ document.addEventListener("DOMContentLoaded", function () {
503
+ const counter = document.getElementById("user-counter");
504
+ if (counter) {
505
+ const target = parseInt(counter.dataset.target) || 0;
506
+ animateCounter(counter, target);
507
+ }
508
+ });
509
+
510
+ // Update counter periodically (every 30 seconds)
511
+ setInterval(function () {
512
+ fetch('{{ url_for("main.get_user_count") }}')
513
+ .then((response) => response.json())
514
+ .then((data) => {
515
+ const counter = document.getElementById("user-counter");
516
+ if (counter && data.count) {
517
+ const currentCount =
518
+ parseInt(
519
+ counter.textContent
520
+ .replace(/\s/g, "")
521
+ .replace(/,/g, ""),
522
+ ) || 0;
523
+ const newCount = data.count;
524
+
525
+ // Only animate if count increased
526
+ if (newCount > currentCount) {
527
+ animateCounter(counter, newCount, 1000);
528
+ }
529
+ }
530
+ })
531
+ .catch((error) => console.log("Error fetching user count:", error));
532
+ }, 30000);
533
+ </script>
534
  {% endblock %}
app/templates/referral.html CHANGED
@@ -1,20 +1,125 @@
1
- {% extends "base.html" %} {% block title %}Mon Profil{% endblock %} {% block
2
  content %}
3
  <div id="view-profile" class="px-5 pt-4">
4
- <!-- Header Profil -->
5
- <div class="flex flex-col items-center mt-8 mb-8">
6
- <div
7
- class="w-24 h-24 bg-card rounded-full border-4 border-gray-800 flex items-center justify-center text-3xl font-bold text-gray-300 mb-4"
 
8
  >
9
- {{ current_user.name[:2].upper() }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  </div>
11
- <h2 class="text-xl font-bold text-white">{{ current_user.name }}</h2>
12
- <p class="text-textMuted">
13
- {{ current_user.country_code }} {{ current_user.phone }}
14
- </p>
15
  </div>
16
 
17
  <div class="space-y-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  <!-- Code Parrainage -->
19
  <div class="bg-card rounded-2xl p-4 border border-gray-800">
20
  <div class="flex items-center justify-between mb-2">
@@ -26,8 +131,15 @@ content %}
26
  </span>
27
  </div>
28
  <p class="text-[10px] text-gray-500 leading-relaxed mb-3">
29
- Partagez votre code unique pour gagner des commissions sur
30
- chaque investissement de vos amis.
 
 
 
 
 
 
 
31
  </p>
32
 
33
  <!-- Lien de parrainage complet -->
@@ -60,20 +172,22 @@ content %}
60
  <span id="copy-text">Copier le lien</span>
61
  </button>
62
 
63
- <button
64
- onclick="shareOnWhatsApp()"
 
65
  class="w-full bg-gradient-to-r from-green-600 to-green-500 hover:from-green-700 hover:to-green-600 text-white font-bold py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center justify-center gap-2 shadow-lg hover:shadow-green-500/50 active:scale-95"
66
  >
67
  <i class="fa-brands fa-whatsapp text-xl"></i>
68
  <span>Partager sur WhatsApp</span>
69
- </button>
70
  </div>
71
  </div>
72
 
73
- <!-- Statistiques Parrainage -->
74
  <div class="grid grid-cols-2 gap-4">
75
  <div class="bg-card rounded-2xl p-4 border border-gray-800">
76
  <p class="text-[10px] text-textMuted uppercase font-bold mb-1">
 
77
  Filleuls
78
  </p>
79
  <p class="text-xl font-bold text-white">
@@ -82,19 +196,96 @@ content %}
82
  </div>
83
  <div class="bg-card rounded-2xl p-4 border border-gray-800">
84
  <p class="text-[10px] text-textMuted uppercase font-bold mb-1">
85
- Gains Bonus
 
86
  </p>
87
- <p class="text-xl font-bold text-primary">
88
- {{ "{:,.0f}".format(current_user.bonus_balance) }} F
 
 
89
  </p>
90
  </div>
91
  </div>
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  <!-- Liste des Filleuls -->
94
  {% if referred_users %}
95
  <div class="bg-card rounded-2xl p-4 border border-gray-800">
96
- <h3 class="text-sm font-bold text-white mb-3">
97
- Mes Amis Parrainés
 
 
 
98
  </h3>
99
  <div class="space-y-3 max-h-60 overflow-y-auto no-scrollbar">
100
  {% for referred in referred_users %}
@@ -118,45 +309,40 @@ content %}
118
  </div>
119
  </div>
120
  <span
121
- class="text-[10px] px-2 py-0.5 rounded-full {% if referred.has_active_subscription %}bg-primary/20 text-primary{% else %}bg-gray-700 text-gray-400{% endif %}"
122
  >
123
- {% if referred.has_active_subscription %}Actif{% else
124
- %}Inactif{% endif %}
 
 
125
  </span>
126
  </div>
127
  {% endfor %}
128
  </div>
129
  </div>
130
- {% endif %}
131
-
132
- <!-- Boutons d'Action -->
133
- <div class="space-y-3 pt-4">
134
- <a
135
- href="#"
136
- class="w-full bg-card hover:bg-cardLight p-4 rounded-xl flex items-center justify-between border border-gray-800 transition group"
137
  >
138
- <div class="flex items-center gap-3 text-gray-300">
139
- <i class="fa-solid fa-headset w-5 text-accent"></i>
140
- <span class="text-sm font-medium">Support Client</span>
141
- </div>
142
- <i
143
- class="fa-solid fa-chevron-right text-xs text-gray-500 group-hover:translate-x-1 transition"
144
- ></i>
145
- </a>
146
-
147
  <a
148
- href="{{ url_for('auth.logout') }}"
149
- class="w-full bg-card hover:bg-danger/10 p-4 rounded-xl flex items-center justify-between border border-gray-800 transition text-danger group"
 
150
  >
151
- <div class="flex items-center gap-3">
152
- <i class="fa-solid fa-power-off w-5"></i>
153
- <span class="text-sm font-medium">Déconnexion</span>
154
- </div>
155
- <i
156
- class="fa-solid fa-arrow-right-from-bracket text-xs opacity-0 group-hover:opacity-100 transition"
157
- ></i>
158
  </a>
159
  </div>
 
160
  </div>
161
  </div>
162
 
@@ -184,22 +370,6 @@ content %}
184
  alert("Impossible de copier le lien");
185
  });
186
  }
187
-
188
- function shareOnWhatsApp() {
189
- const referralLink = document
190
- .getElementById("referral-link")
191
- .textContent.trim();
192
- const message = encodeURIComponent(
193
- `🎁 Rejoins-moi sur {{ app_name }} et commence à investir dans les métaux précieux !\n\n` +
194
- `Utilise mon lien de parrainage pour bénéficier d'un bonus d'inscription :\n` +
195
- `${referralLink}\n\n` +
196
- `💰 Investis intelligemment et gagne de l'argent ensemble !`,
197
- );
198
-
199
- // Ouvrir WhatsApp avec le message
200
- const whatsappUrl = `https://wa.me/?text=${message}`;
201
- window.open(whatsappUrl, "_blank");
202
- }
203
  </script>
204
 
205
  {% endblock %}
 
1
+ {% extends "base.html" %} {% block title %}Parrainage{% endblock %} {% block
2
  content %}
3
  <div id="view-profile" class="px-5 pt-4">
4
+ <!-- Header -->
5
+ <div class="flex items-center gap-3 mb-6">
6
+ <a
7
+ href="{{ url_for('main.dashboard') }}"
8
+ class="w-10 h-10 bg-card rounded-full flex items-center justify-center border border-gray-800 text-gray-400 btn-press"
9
  >
10
+ <i class="fa-solid fa-arrow-left"></i>
11
+ </a>
12
+ <h2 class="text-2xl font-bold text-white">Parrainage</h2>
13
+ </div>
14
+
15
+ <!-- Bannière Commission -->
16
+ <div
17
+ class="bg-gradient-to-br from-green-600/20 to-emerald-600/20 rounded-3xl p-5 border border-green-500/30 mb-6 relative overflow-hidden"
18
+ >
19
+ <div
20
+ class="absolute top-0 right-0 w-32 h-32 bg-green-500/10 rounded-full blur-3xl -mr-16 -mt-16"
21
+ ></div>
22
+ <div class="relative z-10">
23
+ <div class="flex items-center gap-3 mb-4">
24
+ <div
25
+ class="w-12 h-12 bg-green-500/20 rounded-xl flex items-center justify-center"
26
+ >
27
+ <i
28
+ class="fa-solid fa-hand-holding-dollar text-green-400 text-xl"
29
+ ></i>
30
+ </div>
31
+ <div>
32
+ <h3 class="font-bold text-white text-lg">
33
+ Gagnez plus ensemble !
34
+ </h3>
35
+ <p class="text-green-300 text-xs">
36
+ Commissions cumulables sans limite
37
+ </p>
38
+ </div>
39
+ </div>
40
+ <div class="grid grid-cols-2 gap-3">
41
+ <div
42
+ class="bg-black/30 rounded-xl p-3 border border-green-500/20"
43
+ >
44
+ <p
45
+ class="text-[10px] text-gray-400 uppercase tracking-wider mb-1"
46
+ >
47
+ Sur chaque achat
48
+ </p>
49
+ <p class="text-2xl font-bold text-yellow-400">
50
+ {{ purchase_commission_rate }}%
51
+ </p>
52
+ <p class="text-[10px] text-gray-500">de commission</p>
53
+ </div>
54
+ <div
55
+ class="bg-black/30 rounded-xl p-3 border border-green-500/20"
56
+ >
57
+ <p
58
+ class="text-[10px] text-gray-400 uppercase tracking-wider mb-1"
59
+ >
60
+ Gains quotidiens
61
+ </p>
62
+ <p class="text-2xl font-bold text-green-400">
63
+ {{ daily_gain_commission_rate }}%
64
+ </p>
65
+ <p class="text-[10px] text-gray-500">chaque jour</p>
66
+ </div>
67
+ </div>
68
  </div>
 
 
 
 
69
  </div>
70
 
71
  <div class="space-y-4">
72
+ <!-- Statistiques Gains Parrainage -->
73
+ <div class="bg-card rounded-2xl p-4 border border-gray-800">
74
+ <h3
75
+ class="text-sm font-bold text-white mb-4 flex items-center gap-2"
76
+ >
77
+ <i class="fa-solid fa-chart-pie text-primary"></i>
78
+ Mes Gains de Parrainage
79
+ </h3>
80
+ <div class="grid grid-cols-3 gap-3">
81
+ <div
82
+ class="text-center p-3 bg-gray-900/50 rounded-xl border border-gray-800"
83
+ >
84
+ <p
85
+ class="text-[9px] text-gray-500 uppercase tracking-wider mb-1"
86
+ >
87
+ Total Gagné
88
+ </p>
89
+ <p class="text-lg font-bold text-primary">
90
+ {{ "{:,.0f}".format(total_referral_earnings) }}
91
+ </p>
92
+ <p class="text-[10px] text-gray-500">FCFA</p>
93
+ </div>
94
+ <div
95
+ class="text-center p-3 bg-gray-900/50 rounded-xl border border-gray-800"
96
+ >
97
+ <p
98
+ class="text-[9px] text-gray-500 uppercase tracking-wider mb-1"
99
+ >
100
+ Sur Achats
101
+ </p>
102
+ <p class="text-lg font-bold text-yellow-400">
103
+ {{ "{:,.0f}".format(total_purchase_commissions) }}
104
+ </p>
105
+ <p class="text-[10px] text-gray-500">FCFA</p>
106
+ </div>
107
+ <div
108
+ class="text-center p-3 bg-gray-900/50 rounded-xl border border-gray-800"
109
+ >
110
+ <p
111
+ class="text-[9px] text-gray-500 uppercase tracking-wider mb-1"
112
+ >
113
+ Sur Gains
114
+ </p>
115
+ <p class="text-lg font-bold text-green-400">
116
+ {{ "{:,.0f}".format(total_daily_commissions) }}
117
+ </p>
118
+ <p class="text-[10px] text-gray-500">FCFA</p>
119
+ </div>
120
+ </div>
121
+ </div>
122
+
123
  <!-- Code Parrainage -->
124
  <div class="bg-card rounded-2xl p-4 border border-gray-800">
125
  <div class="flex items-center justify-between mb-2">
 
131
  </span>
132
  </div>
133
  <p class="text-[10px] text-gray-500 leading-relaxed mb-3">
134
+ Partagez votre code unique et gagnez
135
+ <strong class="text-yellow-400"
136
+ >{{ purchase_commission_rate }}%</strong
137
+ >
138
+ sur chaque achat de plan +
139
+ <strong class="text-green-400"
140
+ >{{ daily_gain_commission_rate }}%</strong
141
+ >
142
+ sur les gains quotidiens de vos filleuls !
143
  </p>
144
 
145
  <!-- Lien de parrainage complet -->
 
172
  <span id="copy-text">Copier le lien</span>
173
  </button>
174
 
175
+ <a
176
+ href="https://wa.me/?text=J'ai%20d%C3%A9j%C3%A0%20gagn%C3%A9%201000f%20en%20suivant%20ce%20lien.%20Gagnez%20de%20l'argent%20chaque%20jour%20gr%C3%A2ce%20aux%20m%C3%A9taux%20rares.%20%F0%9F%92%B0%0A%0AInscrivez-vous%20ici%20%3A%20{{ request.url_root }}auth/register?ref={{ referral_code }}"
177
+ target="_blank"
178
  class="w-full bg-gradient-to-r from-green-600 to-green-500 hover:from-green-700 hover:to-green-600 text-white font-bold py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center justify-center gap-2 shadow-lg hover:shadow-green-500/50 active:scale-95"
179
  >
180
  <i class="fa-brands fa-whatsapp text-xl"></i>
181
  <span>Partager sur WhatsApp</span>
182
+ </a>
183
  </div>
184
  </div>
185
 
186
+ <!-- Statistiques Filleuls -->
187
  <div class="grid grid-cols-2 gap-4">
188
  <div class="bg-card rounded-2xl p-4 border border-gray-800">
189
  <p class="text-[10px] text-textMuted uppercase font-bold mb-1">
190
+ <i class="fa-solid fa-users text-blue-400 mr-1"></i>
191
  Filleuls
192
  </p>
193
  <p class="text-xl font-bold text-white">
 
196
  </div>
197
  <div class="bg-card rounded-2xl p-4 border border-gray-800">
198
  <p class="text-[10px] text-textMuted uppercase font-bold mb-1">
199
+ <i class="fa-solid fa-user-check text-green-400 mr-1"></i>
200
+ Actifs
201
  </p>
202
+ <p class="text-xl font-bold text-green-400">
203
+ {{
204
+ referred_users|selectattr('has_active_subscription')|list|length
205
+ }}
206
  </p>
207
  </div>
208
  </div>
209
 
210
+ <!-- Comment ça marche -->
211
+ <div class="bg-card rounded-2xl p-4 border border-gray-800">
212
+ <h3
213
+ class="text-sm font-bold text-white mb-3 flex items-center gap-2"
214
+ >
215
+ <i class="fa-solid fa-circle-question text-blue-400"></i>
216
+ Comment ça marche ?
217
+ </h3>
218
+ <div class="space-y-3">
219
+ <div class="flex items-start gap-3">
220
+ <div
221
+ class="w-6 h-6 bg-yellow-500/20 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5"
222
+ >
223
+ <span class="text-yellow-400 text-[10px] font-bold"
224
+ >1</span
225
+ >
226
+ </div>
227
+ <div>
228
+ <p class="text-xs text-white font-medium">
229
+ Partagez votre lien
230
+ </p>
231
+ <p class="text-[10px] text-gray-500">
232
+ Envoyez votre lien à vos amis via WhatsApp ou autre
233
+ </p>
234
+ </div>
235
+ </div>
236
+ <div class="flex items-start gap-3">
237
+ <div
238
+ class="w-6 h-6 bg-blue-500/20 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5"
239
+ >
240
+ <span class="text-blue-400 text-[10px] font-bold"
241
+ >2</span
242
+ >
243
+ </div>
244
+ <div>
245
+ <p class="text-xs text-white font-medium">
246
+ Ils s'inscrivent et achètent
247
+ </p>
248
+ <p class="text-[10px] text-gray-500">
249
+ Vos filleuls créent un compte et souscrivent à un
250
+ plan
251
+ </p>
252
+ </div>
253
+ </div>
254
+ <div class="flex items-start gap-3">
255
+ <div
256
+ class="w-6 h-6 bg-green-500/20 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5"
257
+ >
258
+ <span class="text-green-400 text-[10px] font-bold"
259
+ >3</span
260
+ >
261
+ </div>
262
+ <div>
263
+ <p class="text-xs text-white font-medium">
264
+ Vous gagnez automatiquement
265
+ </p>
266
+ <p class="text-[10px] text-gray-500">
267
+ <strong class="text-yellow-400"
268
+ >{{ purchase_commission_rate }}%</strong
269
+ >
270
+ sur leurs achats +
271
+ <strong class="text-green-400"
272
+ >{{ daily_gain_commission_rate }}%</strong
273
+ >
274
+ sur leurs gains quotidiens
275
+ </p>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ </div>
280
+
281
  <!-- Liste des Filleuls -->
282
  {% if referred_users %}
283
  <div class="bg-card rounded-2xl p-4 border border-gray-800">
284
+ <h3
285
+ class="text-sm font-bold text-white mb-3 flex items-center gap-2"
286
+ >
287
+ <i class="fa-solid fa-user-group text-purple-400"></i>
288
+ Mes Filleuls ({{ referred_users|length }})
289
  </h3>
290
  <div class="space-y-3 max-h-60 overflow-y-auto no-scrollbar">
291
  {% for referred in referred_users %}
 
309
  </div>
310
  </div>
311
  <span
312
+ class="text-[10px] px-2 py-0.5 rounded-full {% if referred.has_active_subscription %}bg-green-500/20 text-green-400{% else %}bg-gray-700 text-gray-400{% endif %}"
313
  >
314
+ {% if referred.has_active_subscription %}
315
+ <i class="fa-solid fa-check-circle mr-1"></i>Actif {%
316
+ else %} <i class="fa-solid fa-clock mr-1"></i>Inactif {%
317
+ endif %}
318
  </span>
319
  </div>
320
  {% endfor %}
321
  </div>
322
  </div>
323
+ {% else %}
324
+ <div
325
+ class="bg-card/50 rounded-2xl p-6 border border-gray-800 border-dashed text-center"
326
+ >
327
+ <div
328
+ class="w-16 h-16 bg-gray-800 rounded-full flex items-center justify-center mx-auto mb-4"
 
329
  >
330
+ <i class="fa-solid fa-user-plus text-gray-600 text-2xl"></i>
331
+ </div>
332
+ <h4 class="text-white font-bold mb-2">Pas encore de filleuls</h4>
333
+ <p class="text-xs text-gray-500 mb-4">
334
+ Partagez votre lien pour commencer à gagner des commissions !
335
+ </p>
 
 
 
336
  <a
337
+ href="https://wa.me/?text=J'ai%20d%C3%A9j%C3%A0%20gagn%C3%A9%201000f%20en%20suivant%20ce%20lien.%20Gagnez%20de%20l'argent%20chaque%20jour%20gr%C3%A2ce%20aux%20m%C3%A9taux%20rares.%20%F0%9F%92%B0%0A%0AInscrivez-vous%20ici%20%3A%20{{ request.url_root }}auth/register?ref={{ referral_code }}"
338
+ target="_blank"
339
+ class="inline-flex items-center gap-2 bg-green-500 text-white font-bold px-6 py-3 rounded-xl text-sm"
340
  >
341
+ <i class="fa-brands fa-whatsapp text-lg"></i>
342
+ Inviter mes amis
 
 
 
 
 
343
  </a>
344
  </div>
345
+ {% endif %}
346
  </div>
347
  </div>
348
 
 
370
  alert("Impossible de copier le lien");
371
  });
372
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  </script>
374
 
375
  {% endblock %}
app/templates/withdraw.html CHANGED
@@ -45,6 +45,72 @@ content %}
45
  {% endif %}
46
  </div>
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  {% if withdrawable_balance >= 500 %}
49
  <form method="POST" class="space-y-5">
50
  {{ form.hidden_tag() }}
@@ -55,13 +121,14 @@ content %}
55
  >
56
  <label
57
  class="text-[10px] text-textMuted block mb-1 font-bold uppercase tracking-widest"
58
- >Montant (FCFA)</label
59
  >
60
  <div class="flex items-center gap-2">
61
  {{ form.amount(class="w-full bg-transparent text-3xl
62
  font-bold text-white focus:outline-none
63
- placeholder-gray-800", placeholder="Min: 500",
64
- type="number") }}
 
65
  </div>
66
  {% if form.amount.errors %}
67
  <p class="text-danger text-[10px] mt-2 font-medium">
@@ -70,6 +137,54 @@ content %}
70
  {% endif %}
71
  </div>
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  <!-- INFOS MOBILE MONEY -->
74
  <div
75
  class="bg-card rounded-2xl p-4 border border-gray-700 space-y-4"
@@ -115,8 +230,9 @@ content %}
115
  ></i>
116
  </div>
117
  <p class="text-[10px] text-gray-500 leading-relaxed">
118
- Retrait sécurisé vers MoMo, Wave ou Orange Money. Délai
119
- de traitement standard : 24h.
 
120
  </p>
121
  </div>
122
  </div>
@@ -126,6 +242,7 @@ content %}
126
  type="submit"
127
  class="w-full btn-press bg-white text-black font-black py-5 rounded-2xl shadow-xl hover:bg-gray-200 transition-colors uppercase tracking-widest text-sm mt-2"
128
  >
 
129
  Confirmer le retrait
130
  </button>
131
  </form>
@@ -139,11 +256,11 @@ content %}
139
  >
140
  <i class="fa-solid fa-triangle-exclamation text-2xl"></i>
141
  </div>
142
- <h3 class="text-white font-bold mb-2">Solde insuffisant</h3>
143
  <p class="text-xs text-gray-500 leading-relaxed mb-6">
144
- Vous avez besoin d'au moins <strong>500 FCFA</strong> pour
145
- initier un retrait. Adoptez de nouveaux plans pour augmenter vos
146
- gains !
147
  </p>
148
  <a
149
  href="{{ url_for('main.market') }}"
@@ -173,4 +290,34 @@ content %}
173
  </div>
174
  </div>
175
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  {% endblock %}
 
45
  {% endif %}
46
  </div>
47
 
48
+ <!-- INFO FRAIS ET DÉLAI -->
49
+ <div
50
+ class="bg-yellow-500/10 rounded-2xl p-4 border border-yellow-500/20"
51
+ >
52
+ <div class="flex items-start gap-3">
53
+ <div
54
+ class="w-10 h-10 bg-yellow-500/20 rounded-full flex items-center justify-center flex-shrink-0"
55
+ >
56
+ <i class="fa-solid fa-percent text-yellow-400"></i>
57
+ </div>
58
+ <div>
59
+ <h4 class="font-bold text-yellow-400 text-sm mb-1">
60
+ Frais de retrait : {{ withdrawal_fee_rate }}%
61
+ </h4>
62
+ <p class="text-[11px] text-gray-400 leading-relaxed">
63
+ Des frais de
64
+ <strong class="text-yellow-400"
65
+ >{{ withdrawal_fee_rate }}%</strong
66
+ >
67
+ sont appliqués sur chaque retrait. {% if
68
+ withdrawable_balance > 0 %}
69
+ <br />
70
+ <span class="text-gray-500">
71
+ Exemple: Pour {{
72
+ "{:,.0f}".format(withdrawable_balance) }} F, vous
73
+ recevrez
74
+ <strong class="text-green-400"
75
+ >{{ "{:,.0f}".format(fee_preview.net) }}
76
+ F</strong
77
+ >
78
+ </span>
79
+ {% endif %}
80
+ </p>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ <div class="bg-blue-500/10 rounded-2xl p-4 border border-blue-500/20">
86
+ <div class="flex items-start gap-3">
87
+ <div
88
+ class="w-10 h-10 bg-blue-500/20 rounded-full flex items-center justify-center flex-shrink-0"
89
+ >
90
+ <i class="fa-solid fa-clock text-blue-400"></i>
91
+ </div>
92
+ <div>
93
+ <h4 class="font-bold text-blue-400 text-sm mb-1">
94
+ Délai de traitement : {{ withdrawal_delay_hours }}h
95
+ </h4>
96
+ <p class="text-[11px] text-gray-400 leading-relaxed">
97
+ Les retraits sont mis en attente pendant
98
+ <strong class="text-blue-400"
99
+ >{{ withdrawal_delay_hours }} heures</strong
100
+ >
101
+ et traités uniquement les
102
+ <strong class="text-blue-400">jours ouvrables</strong>
103
+ (lundi au vendredi).
104
+ <br />
105
+ <span class="text-gray-500"
106
+ >Les retraits du vendredi seront traités le lundi
107
+ suivant.</span
108
+ >
109
+ </p>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
  {% if withdrawable_balance >= 500 %}
115
  <form method="POST" class="space-y-5">
116
  {{ form.hidden_tag() }}
 
121
  >
122
  <label
123
  class="text-[10px] text-textMuted block mb-1 font-bold uppercase tracking-widest"
124
+ >Montant à retirer (FCFA)</label
125
  >
126
  <div class="flex items-center gap-2">
127
  {{ form.amount(class="w-full bg-transparent text-3xl
128
  font-bold text-white focus:outline-none
129
+ placeholder-gray-800", placeholder="Min: " ~
130
+ withdrawal_min_amount, type="number",
131
+ id="withdrawal-amount", min=withdrawal_min_amount) }}
132
  </div>
133
  {% if form.amount.errors %}
134
  <p class="text-danger text-[10px] mt-2 font-medium">
 
137
  {% endif %}
138
  </div>
139
 
140
+ <!-- APERÇU DES FRAIS -->
141
+ <div
142
+ class="bg-card rounded-2xl p-4 border border-gray-700"
143
+ id="fee-preview"
144
+ >
145
+ <div class="grid grid-cols-3 gap-4 text-center">
146
+ <div>
147
+ <p
148
+ class="text-[10px] text-gray-500 uppercase tracking-wider mb-1"
149
+ >
150
+ Montant brut
151
+ </p>
152
+ <p
153
+ class="text-lg font-bold text-white"
154
+ id="gross-amount"
155
+ >
156
+ 0 F
157
+ </p>
158
+ </div>
159
+ <div>
160
+ <p
161
+ class="text-[10px] text-gray-500 uppercase tracking-wider mb-1"
162
+ >
163
+ Frais ({{ withdrawal_fee_rate }}%)
164
+ </p>
165
+ <p
166
+ class="text-lg font-bold text-yellow-400"
167
+ id="fee-amount"
168
+ >
169
+ 0 F
170
+ </p>
171
+ </div>
172
+ <div>
173
+ <p
174
+ class="text-[10px] text-gray-500 uppercase tracking-wider mb-1"
175
+ >
176
+ Vous recevez
177
+ </p>
178
+ <p
179
+ class="text-lg font-bold text-green-400"
180
+ id="net-amount"
181
+ >
182
+ 0 F
183
+ </p>
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
  <!-- INFOS MOBILE MONEY -->
189
  <div
190
  class="bg-card rounded-2xl p-4 border border-gray-700 space-y-4"
 
230
  ></i>
231
  </div>
232
  <p class="text-[10px] text-gray-500 leading-relaxed">
233
+ Retrait sécurisé vers MoMo, Wave ou Orange Money. Le
234
+ montant net (après frais de {{ withdrawal_fee_rate }}%)
235
+ sera envoyé après validation.
236
  </p>
237
  </div>
238
  </div>
 
242
  type="submit"
243
  class="w-full btn-press bg-white text-black font-black py-5 rounded-2xl shadow-xl hover:bg-gray-200 transition-colors uppercase tracking-widest text-sm mt-2"
244
  >
245
+ <i class="fa-solid fa-paper-plane mr-2"></i>
246
  Confirmer le retrait
247
  </button>
248
  </form>
 
256
  >
257
  <i class="fa-solid fa-triangle-exclamation text-2xl"></i>
258
  </div>
259
+ <h4 class="text-white font-bold mb-2">Solde insuffisant</h4>
260
  <p class="text-xs text-gray-500 leading-relaxed mb-6">
261
+ Vous avez besoin d'au moins
262
+ <strong>{{ withdrawal_min_amount }} FCFA</strong> pour initier
263
+ un retrait. Adoptez de nouveaux plans pour augmenter vos gains !
264
  </p>
265
  <a
266
  href="{{ url_for('main.market') }}"
 
290
  </div>
291
  </div>
292
  </div>
293
+ {% endblock %} {% block extra_js %}
294
+ <script>
295
+ // Calculate and display fees in real-time
296
+ const amountInput = document.getElementById("withdrawal-amount");
297
+ const grossDisplay = document.getElementById("gross-amount");
298
+ const feeDisplay = document.getElementById("fee-amount");
299
+ const netDisplay = document.getElementById("net-amount");
300
+
301
+ function formatNumber(num) {
302
+ return num.toLocaleString("fr-FR") + " F";
303
+ }
304
+
305
+ function updateFeePreview() {
306
+ const amount = parseFloat(amountInput.value) || 0;
307
+ const feeRate = {{ withdrawal_fee_rate }} / 100;
308
+ const fee = amount * feeRate;
309
+ const net = amount - fee;
310
+
311
+ if (grossDisplay)
312
+ grossDisplay.textContent = formatNumber(Math.floor(amount));
313
+ if (feeDisplay) feeDisplay.textContent = formatNumber(Math.floor(fee));
314
+ if (netDisplay) netDisplay.textContent = formatNumber(Math.floor(net));
315
+ }
316
+
317
+ if (amountInput) {
318
+ amountInput.addEventListener("input", updateFeePreview);
319
+ // Initialize with current value if any
320
+ updateFeePreview();
321
+ }
322
+ </script>
323
  {% endblock %}
config/__pycache__/config.cpython-314.pyc CHANGED
Binary files a/config/__pycache__/config.cpython-314.pyc and b/config/__pycache__/config.cpython-314.pyc differ
 
config/config.py CHANGED
@@ -10,9 +10,22 @@ class Config:
10
  SQLALCHEMY_TRACK_MODIFICATIONS = False
11
  PERMANENT_SESSION_LIFETIME = timedelta(days=7)
12
  BASE_URL = os.environ.get("BASE_URL", "http://localhost:5000")
13
- REGISTRATION_BONUS = 1000
14
- DAILY_LOGIN_BONUS = 30
15
- REFERRAL_COMMISSION = {"level_1": 25, "level_2": 2, "level_3": 1}
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  # Lygos Payment API Configuration
18
  LYGOS_API_KEY = os.environ.get(
 
10
  SQLALCHEMY_TRACK_MODIFICATIONS = False
11
  PERMANENT_SESSION_LIFETIME = timedelta(days=7)
12
  BASE_URL = os.environ.get("BASE_URL", "http://localhost:5000")
13
+
14
+ # Bonus Configuration
15
+ REGISTRATION_BONUS = 1000 # Bonus d'inscription en FCFA
16
+ DAILY_LOGIN_BONUS = 30 # Bonus de connexion quotidien en FCFA
17
+
18
+ # Referral Commission Configuration (Nouveau système simplifié)
19
+ # Commission sur l'achat de plan du filleul (niveau 1 uniquement)
20
+ REFERRAL_PURCHASE_COMMISSION = 0.15 # 15% sur chaque achat de plan
21
+
22
+ # Commission sur les gains quotidiens du filleul (niveau 1 uniquement)
23
+ REFERRAL_DAILY_GAIN_COMMISSION = 0.03 # 3% sur les gains quotidiens
24
+
25
+ # Withdrawal Configuration
26
+ WITHDRAWAL_FEE_PERCENTAGE = 0.15 # 15% de frais sur les retraits
27
+ WITHDRAWAL_DELAY_HOURS = 24 # Délai de traitement en heures
28
+ WITHDRAWAL_MIN_AMOUNT = 500 # Montant minimum de retrait en FCFA
29
 
30
  # Lygos Payment API Configuration
31
  LYGOS_API_KEY = os.environ.get(
instance/dev_metals_investment.db CHANGED
Binary files a/instance/dev_metals_investment.db and b/instance/dev_metals_investment.db differ
 
scripts/daily_gains.py CHANGED
@@ -1,140 +1,361 @@
1
  #!/usr/bin/env python3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- import sys
4
  import os
 
5
 
6
  sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
7
 
8
- from app import create_app, db
9
- from app.models import UserMetal, User, Transaction, Notification
10
  from datetime import date, datetime, timezone
11
 
 
 
 
 
 
 
 
 
 
12
 
13
  def calculate_daily_gains():
 
 
 
 
14
  app = create_app()
15
  with app.app_context():
16
- print(f"Starting daily gains calculation for {date.today()}")
 
 
 
 
 
 
 
 
17
 
18
  active_metals = UserMetal.query.filter_by(is_active=True).all()
 
 
 
 
 
19
 
20
  for user_metal in active_metals:
21
- if user_metal.last_gain_date < date.today():
22
- if datetime.now(timezone.utc) < user_metal.expiry_date:
23
- metal = user_metal.metal
24
- if metal:
25
- user = user_metal.user
26
-
27
- user.balance += metal.daily_gain
28
- user.total_gains += metal.daily_gain
29
- user_metal.last_gain_date = date.today()
30
-
31
- notification = Notification(
32
- user_id=user.id,
33
- title="Gain Quotidien",
34
- message=f"Vous avez gagné {metal.daily_gain} FCFA aujourd'hui de votre adoption en {metal.name}.",
35
- type="gain",
36
- )
37
- db.session.add(notification)
38
-
39
- transaction = Transaction(
40
- user_id=user.id,
41
- type="gain",
42
- amount=metal.daily_gain,
43
- description=f"Gain quotidien - {metal.name}",
44
- status="completed",
45
- )
46
- db.session.add(transaction)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
- print(
49
- f"Added {metal.daily_gain} FCFA to user {user.phone} for {metal.name}"
50
- )
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- else:
53
- user_metal.is_active = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- notification = Notification(
56
- user_id=user_metal.user_id,
57
- title="Adoption Expirée",
58
- message=f"Votre adoption en {user_metal.metal.name} a expiré.",
59
- type="expiry",
60
  )
61
- db.session.add(notification)
62
 
63
- print(f"Expired investment for user {user_metal.user_id}")
 
64
 
65
  db.session.commit()
66
- print("Daily gains calculation completed.")
67
-
68
-
69
- def process_referral_commissions():
 
 
 
 
 
 
 
 
 
70
  app = create_app()
71
  with app.app_context():
72
- from config.config import config
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- app_config = config["default"]
75
- commissions = app_config.REFERRAL_COMMISSION
76
 
77
- print("Processing referral commissions...")
 
 
 
 
78
 
79
- recent_purchases = Transaction.query.filter(
80
- Transaction.type == "purchase",
81
- Transaction.status == "completed",
82
- Transaction.created_at
83
- >= datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0),
84
- ).all()
85
 
86
- for purchase in recent_purchases:
87
- user = User.query.get(purchase.user_id)
 
 
 
 
 
 
 
 
 
88
 
89
- if user and user.referred_by_code:
90
- process_referral_hierarchy(user, purchase.amount, commissions)
 
 
91
 
92
  db.session.commit()
93
- print("Referral commissions processed.")
 
 
94
 
95
 
96
- def process_referral_hierarchy(user, purchase_amount, commissions):
97
- level = 1
98
- current_user = user
 
 
 
 
99
 
100
- while level <= 3 and current_user.referred_by_code:
101
- referrer = User.query.filter_by(
102
- referral_code=current_user.referred_by_code
103
- ).first()
104
 
105
- if referrer:
106
- commission_rate = commissions[f"level_{level}"] / 100
107
- commission_amount = purchase_amount * commission_rate
 
108
 
109
- referrer.balance += commission_amount
 
 
110
 
111
- notification = Notification(
112
- user_id=referrer.id,
113
- title="Commission de Parrainage",
114
- message=f"Vous avez reçu une commission de niveau {level} de {commission_amount} FCFA.",
115
- type="commission",
116
- )
117
- db.session.add(notification)
118
 
119
- transaction = Transaction(
120
- user_id=referrer.id,
121
- type="commission",
122
- amount=commission_amount,
123
- description=f"Commission niveau {level}",
124
- status="completed",
125
- )
126
- db.session.add(transaction)
127
 
128
- print(
129
- f"Commission level {level} of {commission_amount} FCFA added to {referrer.phone}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  )
 
 
 
 
 
131
 
132
- current_user = referrer
133
- level += 1
134
- else:
135
- break
 
 
 
 
 
 
136
 
137
 
138
  if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
139
  calculate_daily_gains()
140
- process_referral_commissions()
 
 
 
 
 
 
1
  #!/usr/bin/env python3
2
+ """
3
+ Daily Gains Calculator Script
4
+
5
+ This script should be run daily (via cron job or scheduler) to:
6
+ 1. Calculate and distribute daily gains for active investments
7
+ 2. Process referral commissions (configurable % of daily gains to referrers)
8
+ 3. Mark expired investments as inactive
9
+ 4. Auto-process withdrawals that have passed their configurable delay
10
+
11
+ Configuration values are read from config.py:
12
+ - REFERRAL_PURCHASE_COMMISSION: Commission rate on plan purchases (default: 0.15 = 15%)
13
+ - REFERRAL_DAILY_GAIN_COMMISSION: Commission rate on daily gains (default: 0.03 = 3%)
14
+ - WITHDRAWAL_FEE_PERCENTAGE: Fee on withdrawals (default: 0.15 = 15%)
15
+ - WITHDRAWAL_DELAY_HOURS: Hours before auto-processing withdrawals (default: 24)
16
+
17
+ Usage:
18
+ python scripts/daily_gains.py
19
+
20
+ Cron example (run daily at 00:05):
21
+ 5 0 * * * cd /path/to/project && python scripts/daily_gains.py
22
+ """
23
 
 
24
  import os
25
+ import sys
26
 
27
  sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
28
 
 
 
29
  from datetime import date, datetime, timezone
30
 
31
+ from app import create_app, db
32
+ from app.models import (
33
+ Notification,
34
+ ReferralCommission,
35
+ Transaction,
36
+ User,
37
+ UserMetal,
38
+ )
39
+
40
 
41
  def calculate_daily_gains():
42
+ """
43
+ Calculate and distribute daily gains for all active investments.
44
+ Also processes referral commissions on these gains using config rate.
45
+ """
46
  app = create_app()
47
  with app.app_context():
48
+ # Get commission rate from config
49
+ daily_gain_commission_rate = app.config.get(
50
+ "REFERRAL_DAILY_GAIN_COMMISSION", 0.03
51
+ )
52
+
53
+ print(f"[{datetime.now()}] Starting daily gains calculation for {date.today()}")
54
+ print(
55
+ f" Config: Daily gain commission rate = {daily_gain_commission_rate * 100}%"
56
+ )
57
 
58
  active_metals = UserMetal.query.filter_by(is_active=True).all()
59
+ processed_count = 0
60
+ expired_count = 0
61
+ commission_count = 0
62
+ total_gains_distributed = 0
63
+ total_commissions_paid = 0
64
 
65
  for user_metal in active_metals:
66
+ # Skip if already processed today
67
+ if user_metal.last_gain_date >= date.today():
68
+ continue
69
+
70
+ # Check if investment has expired
71
+ if datetime.now(timezone.utc) >= user_metal.expiry_date:
72
+ user_metal.is_active = False
73
+ expired_count += 1
74
+
75
+ notification = Notification(
76
+ user_id=user_metal.user_id,
77
+ title="Adoption Expirée",
78
+ message=f"Votre adoption en {user_metal.metal.name} a expiré. Vous avez reçu un total de {user_metal.metal.total_return:.0f} FCFA de gains.",
79
+ type="expiry",
80
+ )
81
+ db.session.add(notification)
82
+ print(
83
+ f" - Expired investment for user {user_metal.user_id} ({user_metal.metal.name})"
84
+ )
85
+ continue
86
+
87
+ metal = user_metal.metal
88
+ if not metal:
89
+ continue
90
+
91
+ user = user_metal.user
92
+ if not user:
93
+ continue
94
+
95
+ daily_gain = metal.daily_gain
96
+
97
+ # Add daily gain to user balance
98
+ user.balance += daily_gain
99
+ user.total_gains += daily_gain
100
+ user_metal.last_gain_date = date.today()
101
+ total_gains_distributed += daily_gain
102
+
103
+ # Create notification for user
104
+ notification = Notification(
105
+ user_id=user.id,
106
+ title="Gain Quotidien 💰",
107
+ message=f"Vous avez gagné {daily_gain:.0f} FCFA aujourd'hui de votre adoption en {metal.name}.",
108
+ type="gain",
109
+ )
110
+ db.session.add(notification)
111
 
112
+ # Create transaction record
113
+ transaction = Transaction(
114
+ user_id=user.id,
115
+ type="gain",
116
+ amount=daily_gain,
117
+ description=f"Gain quotidien - {metal.name}",
118
+ status="completed",
119
+ )
120
+ db.session.add(transaction)
121
+
122
+ processed_count += 1
123
+ print(
124
+ f" + Added {daily_gain:.0f} FCFA to user {user.phone} for {metal.name}"
125
+ )
126
 
127
+ # Process referral commission using config rate
128
+ referrer = user.get_referrer()
129
+ if referrer:
130
+ try:
131
+ # Calculate commission using config rate
132
+ commission_amount = daily_gain * daily_gain_commission_rate
133
+
134
+ # Create the commission record
135
+ commission = ReferralCommission(
136
+ referrer_id=referrer.id,
137
+ referred_user_id=user.id,
138
+ level=1,
139
+ commission_type="daily_gain",
140
+ commission_percentage=daily_gain_commission_rate * 100,
141
+ commission_amount=commission_amount,
142
+ gain_amount=daily_gain,
143
+ )
144
+ db.session.add(commission)
145
+
146
+ # Add commission to referrer's balance
147
+ referrer.balance += commission_amount
148
+ referrer.referral_earnings = (
149
+ referrer.referral_earnings or 0
150
+ ) + commission_amount
151
+
152
+ total_commissions_paid += commission_amount
153
+
154
+ # Only notify for significant amounts to avoid spam
155
+ if commission_amount >= 5:
156
+ referrer_notification = Notification(
157
+ user_id=referrer.id,
158
+ title="Commission sur Gains",
159
+ message=f"Vous avez reçu {commission_amount:.0f} FCFA ({daily_gain_commission_rate * 100:.0f}%) sur les gains de {user.name}.",
160
+ type="referral",
161
+ )
162
+ db.session.add(referrer_notification)
163
 
164
+ commission_count += 1
165
+ print(
166
+ f" Commission of {commission_amount:.2f} FCFA to referrer {referrer.phone}"
 
 
167
  )
 
168
 
169
+ except Exception as e:
170
+ print(f" ! Error processing referral commission: {e}")
171
 
172
  db.session.commit()
173
+ print(f"[{datetime.now()}] Daily gains completed:")
174
+ print(f" - Processed: {processed_count} investments")
175
+ print(f" - Expired: {expired_count} investments")
176
+ print(f" - Commissions: {commission_count} paid")
177
+ print(f" - Total gains distributed: {total_gains_distributed:.0f} FCFA")
178
+ print(f" - Total commissions paid: {total_commissions_paid:.0f} FCFA")
179
+
180
+
181
+ def auto_process_withdrawals():
182
+ """
183
+ Auto-approve withdrawals that have passed their configurable delay without admin action.
184
+ Only runs on business days (Monday to Friday).
185
+ """
186
  app = create_app()
187
  with app.app_context():
188
+ now = datetime.now(timezone.utc)
189
+
190
+ # Get config values
191
+ withdrawal_fee_percentage = app.config.get("WITHDRAWAL_FEE_PERCENTAGE", 0.15)
192
+ withdrawal_delay_hours = app.config.get("WITHDRAWAL_DELAY_HOURS", 24)
193
+
194
+ # Skip weekends
195
+ if now.weekday() in [5, 6]: # Saturday = 5, Sunday = 6
196
+ print(f"[{datetime.now()}] Skipping auto-withdrawal processing (weekend)")
197
+ return
198
+
199
+ print(f"[{datetime.now()}] Starting auto-withdrawal processing")
200
+ print(
201
+ f" Config: Fee = {withdrawal_fee_percentage * 100}%, Delay = {withdrawal_delay_hours}h"
202
+ )
203
+
204
+ # Find pending withdrawals that can be auto-processed
205
+ pending_withdrawals = Transaction.query.filter(
206
+ Transaction.type == "withdrawal",
207
+ Transaction.status == "pending",
208
+ Transaction.admin_action.is_(None),
209
+ Transaction.scheduled_process_time <= now,
210
+ ).all()
211
 
212
+ processed_count = 0
213
+ total_fees_collected = 0
214
 
215
+ for transaction in pending_withdrawals:
216
+ transaction.status = "approved"
217
+ transaction.processed_at = now
218
+ transaction.admin_action = "auto_approved"
219
+ transaction.admin_action_time = now
220
 
221
+ net_amount = transaction.net_amount or transaction.amount
222
+ fee_amount = transaction.fee_amount or 0
223
+ total_fees_collected += fee_amount
 
 
 
224
 
225
+ notification = Notification(
226
+ user_id=transaction.user_id,
227
+ title="Retrait Traité ✅",
228
+ message=(
229
+ f"Votre retrait de {transaction.amount:.0f} FCFA a été traité automatiquement.\n"
230
+ f"Frais ({withdrawal_fee_percentage * 100:.0f}%): {fee_amount:.0f} FCFA\n"
231
+ f"Montant envoyé: {net_amount:.0f} FCFA"
232
+ ),
233
+ type="withdrawal",
234
+ )
235
+ db.session.add(notification)
236
 
237
+ processed_count += 1
238
+ print(
239
+ f" + Auto-approved withdrawal #{transaction.id} for user {transaction.user_id} ({net_amount:.0f} FCFA net)"
240
+ )
241
 
242
  db.session.commit()
243
+ print(f"[{datetime.now()}] Auto-withdrawal processing completed:")
244
+ print(f" - Processed: {processed_count} withdrawals")
245
+ print(f" - Total fees collected: {total_fees_collected:.0f} FCFA")
246
 
247
 
248
+ def cleanup_old_notifications():
249
+ """
250
+ Optional: Clean up old read notifications (older than 30 days).
251
+ """
252
+ app = create_app()
253
+ with app.app_context():
254
+ from datetime import timedelta
255
 
256
+ cutoff_date = datetime.now(timezone.utc) - timedelta(days=30)
 
 
 
257
 
258
+ old_notifications = Notification.query.filter(
259
+ Notification.is_read == True,
260
+ Notification.created_at < cutoff_date,
261
+ ).all()
262
 
263
+ count = len(old_notifications)
264
+ for notification in old_notifications:
265
+ db.session.delete(notification)
266
 
267
+ db.session.commit()
268
+ print(f"[{datetime.now()}] Cleaned up {count} old notifications")
 
 
 
 
 
269
 
 
 
 
 
 
 
 
 
270
 
271
+ def print_config_summary():
272
+ """Print current configuration values."""
273
+ app = create_app()
274
+ with app.app_context():
275
+ print("\n" + "=" * 60)
276
+ print("CONFIGURATION SUMMARY")
277
+ print("=" * 60)
278
+ print(f"Registration Bonus: {app.config.get('REGISTRATION_BONUS', 1000)} FCFA")
279
+ print(f"Daily Login Bonus: {app.config.get('DAILY_LOGIN_BONUS', 30)} FCFA")
280
+ print(
281
+ f"Referral Purchase Commission: {app.config.get('REFERRAL_PURCHASE_COMMISSION', 0.15) * 100}%"
282
+ )
283
+ print(
284
+ f"Referral Daily Gain Commission: {app.config.get('REFERRAL_DAILY_GAIN_COMMISSION', 0.03) * 100}%"
285
+ )
286
+ print(
287
+ f"Withdrawal Fee: {app.config.get('WITHDRAWAL_FEE_PERCENTAGE', 0.15) * 100}%"
288
+ )
289
+ print(f"Withdrawal Delay: {app.config.get('WITHDRAWAL_DELAY_HOURS', 24)} hours")
290
+ print(
291
+ f"Withdrawal Min Amount: {app.config.get('WITHDRAWAL_MIN_AMOUNT', 500)} FCFA"
292
+ )
293
+ print("=" * 60 + "\n")
294
+
295
+
296
+ def print_platform_summary():
297
+ """Print a summary of the platform statistics."""
298
+ app = create_app()
299
+ with app.app_context():
300
+ total_users = User.query.count()
301
+ active_investments = UserMetal.query.filter_by(is_active=True).count()
302
+ pending_withdrawals = Transaction.query.filter_by(
303
+ type="withdrawal", status="pending"
304
+ ).count()
305
+
306
+ total_gains_today = (
307
+ db.session.query(db.func.sum(Transaction.amount))
308
+ .filter(
309
+ Transaction.type == "gain",
310
+ Transaction.created_at
311
+ >= datetime.now(timezone.utc).replace(
312
+ hour=0, minute=0, second=0, microsecond=0
313
+ ),
314
+ )
315
+ .scalar()
316
+ or 0
317
+ )
318
+
319
+ total_commissions_today = (
320
+ db.session.query(db.func.sum(ReferralCommission.commission_amount))
321
+ .filter(
322
+ ReferralCommission.created_at
323
+ >= datetime.now(timezone.utc).replace(
324
+ hour=0, minute=0, second=0, microsecond=0
325
+ ),
326
  )
327
+ .scalar()
328
+ or 0
329
+ )
330
+
331
+ total_withdrawal_fees = Transaction.get_total_withdrawal_fees()
332
 
333
+ print("\n" + "=" * 60)
334
+ print("PLATFORM SUMMARY")
335
+ print("=" * 60)
336
+ print(f"Total Users: {total_users}")
337
+ print(f"Active Investments: {active_investments}")
338
+ print(f"Pending Withdrawals: {pending_withdrawals}")
339
+ print(f"Total Gains Today: {total_gains_today:.0f} FCFA")
340
+ print(f"Total Commissions Today: {total_commissions_today:.0f} FCFA")
341
+ print(f"Total Withdrawal Fees Collected: {total_withdrawal_fees:.0f} FCFA")
342
+ print("=" * 60 + "\n")
343
 
344
 
345
  if __name__ == "__main__":
346
+ print("\n" + "=" * 60)
347
+ print("DAILY PROCESSING SCRIPT")
348
+ print(f"Started at: {datetime.now()}")
349
+ print("=" * 60 + "\n")
350
+
351
+ # Print current configuration
352
+ print_config_summary()
353
+
354
+ # Run all daily tasks
355
  calculate_daily_gains()
356
+ auto_process_withdrawals()
357
+ cleanup_old_notifications()
358
+ print_platform_summary()
359
+
360
+ print("All daily tasks completed successfully!")
361
+ print(f"Finished at: {datetime.now()}\n")