Upload 67 files
Browse files- app/__pycache__/__init__.cpython-314.pyc +0 -0
- app/models/__init__.py +259 -8
- app/models/__pycache__/__init__.cpython-314.pyc +0 -0
- app/routes/__pycache__/admin_routes.cpython-314.pyc +0 -0
- app/routes/__pycache__/main.cpython-314.pyc +0 -0
- app/routes/__pycache__/payments.cpython-314.pyc +0 -0
- app/routes/admin_routes.py +202 -22
- app/routes/main.py +192 -8
- app/routes/payments.py +146 -45
- app/templates/admin/base.html +173 -117
- app/templates/admin/dashboard.html +252 -61
- app/templates/admin/referral_commissions.html +217 -0
- app/templates/admin/settings.html +79 -7
- app/templates/admin/withdrawals.html +183 -60
- app/templates/dashboard.html +165 -3
- app/templates/index.html +88 -14
- app/templates/referral.html +235 -65
- app/templates/withdraw.html +156 -9
- config/__pycache__/config.cpython-314.pyc +0 -0
- config/config.py +16 -3
- instance/dev_metals_investment.db +0 -0
- scripts/daily_gains.py +315 -94
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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 47 |
|
| 48 |
@staticmethod
|
| 49 |
def get_app_logo():
|
| 50 |
"""Get the application logo URL"""
|
| 51 |
-
return AppSettings.get_setting(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 + (
|
|
|
|
|
|
|
| 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=
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
from werkzeug.utils import secure_filename
|
| 3 |
from datetime import datetime, timezone
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
from app import db
|
| 5 |
from app.models import (
|
| 6 |
-
|
| 7 |
Metal,
|
| 8 |
-
UserMetal,
|
| 9 |
-
Transaction,
|
| 10 |
-
ReferralCommission,
|
| 11 |
Notification,
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
| 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 == "
|
| 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=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
type="withdrawal",
|
| 241 |
)
|
| 242 |
db.session.add(notification)
|
| 243 |
|
| 244 |
-
flash(
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
type="withdrawal",
|
| 211 |
)
|
| 212 |
db.session.add(notification)
|
| 213 |
|
| 214 |
db.session.commit()
|
| 215 |
|
| 216 |
-
flash(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
from app import db
|
| 9 |
-
from app.models import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
bp = Blueprint('payments', __name__)
|
| 12 |
|
| 13 |
-
def create_lygos_payment_link(
|
|
|
|
|
|
|
| 14 |
payload = {
|
| 15 |
"amount": int(amount),
|
| 16 |
"shop_name": shop_name,
|
| 17 |
"message": message,
|
| 18 |
"order_id": order_id,
|
| 19 |
-
"success_url": success_url
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
headers = {
|
| 24 |
-
"api-key": current_app.config.get(
|
| 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(
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 58 |
message=message,
|
| 59 |
-
order_id=order_id
|
| 60 |
)
|
| 61 |
|
| 62 |
if payment_link:
|
| 63 |
return redirect(payment_link)
|
| 64 |
else:
|
| 65 |
-
flash(
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
|
|
|
|
| 69 |
def success():
|
| 70 |
-
payment_provider = request.args.get(
|
| 71 |
-
order_id = request.args.get(
|
| 72 |
|
| 73 |
-
current_app.logger.info(
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
if payment_provider ==
|
| 76 |
# Verify payment status with Lygos API
|
| 77 |
-
headers = {"api-key": current_app.config.get(
|
| 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(
|
| 88 |
-
match = re.match(r
|
| 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(
|
|
|
|
|
|
|
| 98 |
if not existing_tx:
|
| 99 |
# Check if this is the user's first subscription and unlock bonus
|
| 100 |
-
if
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
| 143 |
else:
|
| 144 |
flash("Cette commande a déjà été traitée.", "info")
|
| 145 |
-
return redirect(url_for(
|
| 146 |
|
| 147 |
else:
|
| 148 |
-
flash(
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
| 154 |
|
| 155 |
-
@bp.route(
|
| 156 |
def error():
|
| 157 |
flash("Le paiement a échoué ou a été annulé.", "error")
|
| 158 |
-
return redirect(url_for(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<!
|
| 2 |
<html lang="fr">
|
| 3 |
-
<head>
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 57 |
-
|
| 58 |
-
<
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
</a>
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
-
<a
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
| 90 |
</a>
|
| 91 |
-
</div>
|
| 92 |
-
</nav>
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
</div>
|
| 101 |
-
</aside>
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 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 |
-
</
|
| 119 |
|
| 120 |
-
<!--
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
{% if messages %}
|
| 123 |
<div class="px-8 pt-4">
|
| 124 |
{% for category, message in messages %}
|
| 125 |
-
<div
|
|
|
|
|
|
|
| 126 |
<div class="flex items-center">
|
| 127 |
-
<i
|
|
|
|
|
|
|
| 128 |
{{ message }}
|
| 129 |
</div>
|
| 130 |
</div>
|
| 131 |
{% endfor %}
|
| 132 |
</div>
|
| 133 |
-
{% endif %}
|
| 134 |
-
{% endwith %}
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
</main>
|
| 140 |
</div>
|
| 141 |
-
</div>
|
| 142 |
|
| 143 |
-
|
| 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 |
-
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
| 9 |
-
<!--
|
| 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
|
| 14 |
-
<h3 class="text-3xl font-bold text-white mt-1">
|
|
|
|
|
|
|
| 15 |
</div>
|
| 16 |
-
<div
|
| 17 |
-
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
| 31 |
</div>
|
| 32 |
-
<div
|
| 33 |
-
|
|
|
|
|
|
|
| 34 |
</div>
|
| 35 |
</div>
|
| 36 |
-
<a
|
|
|
|
|
|
|
|
|
|
| 37 |
Voir les demandes <i class="fas fa-arrow-right ml-1"></i>
|
| 38 |
</a>
|
| 39 |
</div>
|
| 40 |
|
| 41 |
-
<!--
|
| 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">
|
| 46 |
-
<h3 class="text-2xl font-bold text-white mt-1">
|
|
|
|
|
|
|
|
|
|
| 47 |
</div>
|
| 48 |
-
<div
|
| 49 |
-
|
|
|
|
|
|
|
| 50 |
</div>
|
| 51 |
</div>
|
|
|
|
| 52 |
</div>
|
| 53 |
|
| 54 |
-
<!--
|
| 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">
|
| 59 |
-
<h3 class="text-2xl font-bold text-white mt-1">
|
|
|
|
|
|
|
|
|
|
| 60 |
</div>
|
| 61 |
-
<div
|
| 62 |
-
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
|
|
|
|
|
|
|
|
|
| 96 |
</td>
|
| 97 |
<td class="px-6 py-4 whitespace-nowrap">
|
| 98 |
-
<span
|
| 99 |
-
{% if transaction.type == 'deposit' %}bg-green-500/20 text-green-400
|
| 100 |
-
|
| 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 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
{% endif %}
|
| 115 |
</span>
|
| 116 |
</td>
|
| 117 |
<td class="px-6 py-4 whitespace-nowrap">
|
| 118 |
-
<span
|
| 119 |
-
{% if transaction.type == 'withdrawal' %}-{% else %}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
</span>
|
| 121 |
</td>
|
| 122 |
<td class="px-6 py-4 whitespace-nowrap">
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
{
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 134 |
</span>
|
| 135 |
</td>
|
| 136 |
-
<td
|
|
|
|
|
|
|
| 137 |
{{ transaction.created_at.strftime('%d/%m/%Y %H:%M') }}
|
| 138 |
</td>
|
| 139 |
</tr>
|
| 140 |
{% else %}
|
| 141 |
<tr>
|
| 142 |
-
<td colspan="
|
| 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
|
| 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
|
| 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-
|
| 27 |
-
<i class="fas fa-font text-
|
| 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-
|
| 56 |
-
<i class="fas fa-image text-
|
| 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
|
| 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="
|
| 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 |
-
|
| 20 |
-
|
| 21 |
-
<a href="{{ url_for('admin_panel.
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 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-
|
| 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">
|
| 79 |
-
<p class="text-
|
| 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-
|
| 95 |
-
<th class="px-
|
| 96 |
-
<th class="px-
|
| 97 |
-
<th class="px-
|
| 98 |
-
<th class="px-
|
| 99 |
-
<th class="px-
|
| 100 |
-
<th class="px-
|
|
|
|
|
|
|
| 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-
|
| 107 |
#{{ withdrawal.id }}
|
| 108 |
</td>
|
| 109 |
-
<td class="px-
|
| 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-
|
| 121 |
-
<span class="text-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
</td>
|
| 123 |
-
<td class="px-
|
| 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-
|
| 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-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
</td>
|
| 156 |
-
<td class="px-
|
| 157 |
{% if withdrawal.status == 'pending' %}
|
| 158 |
-
<div class="flex
|
| 159 |
<!-- Approve Button -->
|
| 160 |
<a href="{{ url_for('admin_panel.process_withdrawal', transaction_id=withdrawal.id, action='approve') }}"
|
| 161 |
-
onclick="return confirm('
|
| 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('
|
| 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 |
-
<
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
{% endif %}
|
| 181 |
</td>
|
| 182 |
</tr>
|
| 183 |
{% else %}
|
| 184 |
<tr>
|
| 185 |
-
<td colspan="
|
| 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 |
-
<!--
|
| 203 |
-
<div class="mt-6
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
<div class="flex items-start">
|
| 205 |
-
<i class="fas fa-info-circle text-
|
| 206 |
<div class="text-sm text-gray-300">
|
| 207 |
-
<p class="font-medium text-
|
| 208 |
<ul class="space-y-1 text-gray-400">
|
| 209 |
-
<li>• <strong>Approuver</strong> :
|
| 210 |
-
<li>• <strong>Rejeter</strong> : Le retrait est annulé et le montant est automatiquement remboursé sur le solde
|
| 211 |
-
<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-
|
| 269 |
<p class="text-xs text-gray-400 leading-relaxed">
|
| 270 |
-
Les retraits sont
|
| 271 |
-
|
| 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
|
| 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="
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 283 |
<span class="text-gray-300"
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 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 |
-
|
| 294 |
-
|
| 295 |
-
|
|
|
|
| 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
|
| 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 %}
|
| 2 |
content %}
|
| 3 |
<div id="view-profile" class="px-5 pt-4">
|
| 4 |
-
<!-- Header
|
| 5 |
-
<div class="flex
|
| 6 |
-
<
|
| 7 |
-
|
|
|
|
| 8 |
>
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 64 |
-
|
|
|
|
| 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 |
-
</
|
| 70 |
</div>
|
| 71 |
</div>
|
| 72 |
|
| 73 |
-
<!-- Statistiques
|
| 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 |
-
|
|
|
|
| 86 |
</p>
|
| 87 |
-
<p class="text-xl font-bold text-
|
| 88 |
-
{{
|
|
|
|
|
|
|
| 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
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 122 |
>
|
| 123 |
-
{% if referred.has_active_subscription %}
|
| 124 |
-
|
|
|
|
|
|
|
| 125 |
</span>
|
| 126 |
</div>
|
| 127 |
{% endfor %}
|
| 128 |
</div>
|
| 129 |
</div>
|
| 130 |
-
{%
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
<
|
| 135 |
-
|
| 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 |
-
<
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
></i>
|
| 145 |
-
</a>
|
| 146 |
-
|
| 147 |
<a
|
| 148 |
-
href="{{
|
| 149 |
-
|
|
|
|
| 150 |
>
|
| 151 |
-
<
|
| 152 |
-
|
| 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:
|
| 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.
|
| 119 |
-
de
|
|
|
|
| 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 |
-
<
|
| 143 |
<p class="text-xs text-gray-500 leading-relaxed mb-6">
|
| 144 |
-
Vous avez besoin d'au moins
|
| 145 |
-
|
| 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 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
active_metals = UserMetal.query.filter_by(is_active=True).all()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
for user_metal in active_metals:
|
| 21 |
-
if
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
message=f"Votre adoption en {user_metal.metal.name} a expiré.",
|
| 59 |
-
type="expiry",
|
| 60 |
)
|
| 61 |
-
db.session.add(notification)
|
| 62 |
|
| 63 |
-
|
|
|
|
| 64 |
|
| 65 |
db.session.commit()
|
| 66 |
-
print("Daily gains
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
app = create_app()
|
| 71 |
with app.app_context():
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
Transaction.created_at
|
| 83 |
-
>= datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0),
|
| 84 |
-
).all()
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
| 91 |
|
| 92 |
db.session.commit()
|
| 93 |
-
print("
|
|
|
|
|
|
|
| 94 |
|
| 95 |
|
| 96 |
-
def
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
-
|
| 101 |
-
referrer = User.query.filter_by(
|
| 102 |
-
referral_code=current_user.referred_by_code
|
| 103 |
-
).first()
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
| 108 |
|
| 109 |
-
|
|
|
|
|
|
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 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 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
|
| 138 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
calculate_daily_gains()
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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")
|