diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -12,7 +12,6 @@ import threading import random import pytz import uuid -import string from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError @@ -38,22 +37,19 @@ visitor_data_cache = {} def generate_unique_id(all_data): while True: + # Generate a 5-digit numeric ID new_id = str(random.randint(10000, 99999)) - if new_id not in all_data: - return new_id - -def generate_referral_code(all_data): - while True: - code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) - is_unique = True - for key, user_data in all_data.items(): - if key == "organization_details": - continue - if user_data.get('referral_code') == code: - is_unique = False + + # Check if this ID exists as a client ID or a partner code + is_duplicate = False + for user_id, user_data in all_data.items(): + if user_id == "organization_details": continue + if user_id == new_id or user_data.get('partner_code') == new_id: + is_duplicate = True break - if is_unique: - return code + + if not is_duplicate: + return new_id def download_data_from_hf(): global visitor_data_cache @@ -81,6 +77,68 @@ def download_data_from_hf(): except (FileNotFoundError, json.JSONDecodeError) as e: logging.error(f"Error reading downloaded data file: {e}. Starting with empty cache.") visitor_data_cache = {} + + # Ensure organization_details and initial user fields exist after loading + if "organization_details" not in visitor_data_cache: + visitor_data_cache["organization_details"] = {} + if 'referral_percentage' not in visitor_data_cache["organization_details"]: + visitor_data_cache["organization_details"]['referral_percentage'] = 2.0 # Default 2% + + # Ensure all user entries have required fields (partner_code, referred_by, referred_users, invoices) + # This is a migration step for existing data + users_to_update = {} + for user_id, user_data in visitor_data_cache.items(): + if user_id == "organization_details": continue + updated_user_data = user_data.copy() + + if 'partner_code' not in updated_user_data or not updated_user_data['partner_code']: + updated_user_data['partner_code'] = generate_unique_id(visitor_data_cache) # Generate unique code + if 'referred_by' not in updated_user_data: + updated_user_data['referred_by'] = None + if 'referred_users' not in updated_user_data or not isinstance(updated_user_data['referred_users'], list): + updated_user_data['referred_users'] = [] + if 'invoices' not in updated_user_data or not isinstance(updated_user_data['invoices'], list): + updated_user_data['invoices'] = [] + + if updated_user_data != user_data: + users_to_update[user_id] = updated_user_data + + for user_id, user_data in users_to_update.items(): + visitor_data_cache[user_id] = user_data + logging.info(f"Migrated user data for ID {user_id}") + + if users_to_update: + # Rebuild referred_users lists based on current referred_by fields + temp_referred_users = {} + for user_id, user_data in visitor_data_cache.items(): + if user_id == "organization_details": continue + user_data['referred_users'] = [] # Clear old list + referred_by_code = user_data.get('referred_by') + if referred_by_code: + temp_referred_users.setdefault(referred_by_code, []).append(user_id) + + # Update the actual referred_users lists + for referrer_code, referred_ids in temp_referred_users.items(): + found_referrer = False + for user_id, user_data in visitor_data_cache.items(): + if user_id == "organization_details": continue + if user_data.get('partner_code') == referrer_code: + user_data['referred_users'] = referred_ids + found_referrer = True + break + if not found_referrer: + logging.warning(f"Referrer with code {referrer_code} not found for referred users {referred_ids}. Clearing referred_by.") + # Clear referred_by for these users if referrer doesn't exist + for referred_id in referred_ids: + if referred_id in visitor_data_cache and referred_id != "organization_details": + visitor_data_cache[referred_id]['referred_by'] = None + + + logging.info("Rebuilt referred_users lists.") + # Save the potentially updated cache after migration and rebuild + save_visitor_data(visitor_data_cache) + + return True except RepositoryNotFoundError: logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.") @@ -105,15 +163,76 @@ def load_visitor_data(): except Exception as e: logging.error(f"Unexpected error loading visitor data: {e}") visitor_data_cache = {"organization_details": {}} - + + # Ensure organization_details key exists if "organization_details" not in visitor_data_cache: visitor_data_cache["organization_details"] = {} + if 'referral_percentage' not in visitor_data_cache["organization_details"]: + visitor_data_cache["organization_details"]['referral_percentage'] = 2.0 # Default + + # Ensure all user entries have required fields on load if not already present + users_to_update = {} + for user_id, user_data in visitor_data_cache.items(): + if user_id == "organization_details": continue + updated_user_data = user_data.copy() # Copy to detect changes + + if 'partner_code' not in updated_user_data or not updated_user_data['partner_code']: + # Generating a unique code here on load might be problematic if called frequently without saving. + # Better to ensure it's generated on user creation. If missing, leave as None or generate and mark for save. + # Let's ensure generation only happens on creation (verify or add_client). + # If it's missing on load, maybe just log a warning or assign None. Let's assign None for now. + # The initial download/load function handles generation for missing codes on migration. + updated_user_data['partner_code'] = updated_user_data.get('partner_code') # Keep existing or None + + if 'referred_by' not in updated_user_data: + updated_user_data['referred_by'] = None + if 'referred_users' not in updated_user_data or not isinstance(updated_user_data['referred_users'], list): + updated_user_data['referred_users'] = [] + if 'invoices' not in updated_user_data or not isinstance(updated_user_data['invoices'], list): + updated_user_data['invoices'] = [] + if 'debt_history' not in updated_user_data or not isinstance(updated_user_data['debt_history'], list): + updated_user_data['debt_history'] = [] + if 'history' not in updated_user_data or not isinstance(updated_user_data['history'], list): + updated_user_data['history'] = [] + + if updated_user_data != user_data: + users_to_update[user_id] = updated_user_data # Mark for potential update + + if users_to_update: + logging.warning(f"Missing keys in {len(users_to_update)} user entries. Updating cache but not saving automatically.") + for user_id, updated_data in users_to_update.items(): + visitor_data_cache[user_id] = updated_data + + # Rebuild referred_users lists on every load just to be safe + temp_referred_users = {} + for user_id, user_data in visitor_data_cache.items(): + if user_id == "organization_details": continue + user_data['referred_users'] = [] # Clear old list + referred_by_code = user_data.get('referred_by') + if referred_by_code: + temp_referred_users.setdefault(referred_by_code, []).append(user_id) + + # Update the actual referred_users lists + for referrer_code, referred_ids in temp_referred_users.items(): + found_referrer = False + for user_id, user_data in visitor_data_cache.items(): + if user_id == "organization_details": continue + if user_data.get('partner_code') == referrer_code: + user_data['referred_users'] = referred_ids + found_referrer = True + break + if not found_referrer: + logging.warning(f"Referrer with code {referrer_code} not found during referred_users rebuild.") + # Don't clear referred_by here, it's safer to keep the link even if referrer was deleted manually return visitor_data_cache -def save_visitor_data(data): +def save_visitor_data(data_to_save): with _data_lock: try: + # Ensure visitor_data_cache is the source of truth before saving + # In calling code, modifications should happen directly on visitor_data_cache + # This function now simply dumps the current state of visitor_data_cache with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4) logging.info(f"Visitor data successfully saved to {DATA_FILE}.") @@ -132,6 +251,7 @@ def upload_data_to_hf(): try: api = HfApi() with _data_lock: + # Check file size *within* the lock just before reading file_content_exists = os.path.getsize(DATA_FILE) > 0 if not file_content_exists: logging.warning(f"{DATA_FILE} is empty. Skipping upload.") @@ -219,6 +339,8 @@ TEMPLATE = """ --shadow-glow: 0 0 35px var(--shadow-color); --shadow-color-red: rgba(244, 67, 54, 0.15); --shadow-glow-red: 0 0 35px var(--shadow-color-red); + --button-bg: var(--brand-yellow); + --button-text: var(--brand-black); } * { box-sizing: border-box; margin: 0; padding: 0; } html, body { @@ -331,15 +453,20 @@ TEMPLATE = """ letter-spacing: -2px; line-height: 1; } - .client-id-card { + .client-id-card, .partner-section, .referred-by-section { background-color: var(--card-bg); border-radius: var(--border-radius); padding: var(--padding-m); display: flex; + flex-direction: column; /* Stack content vertically */ + gap: 8px; + } + .client-id-card { + flex-direction: row; /* Keep client ID horizontal */ justify-content: space-between; align-items: center; } - .client-id-label { + .client-id-label, .partner-label, .referred-by-label { font-weight: 500; color: var(--text-secondary-color); } @@ -352,6 +479,87 @@ TEMPLATE = """ padding: 4px 10px; border-radius: 8px; } + .partner-code-display, .referred-by-value { + font-size: 1.1em; + font-weight: 700; + color: var(--text-color); + } + .partner-code-display { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + background-color: rgba(255,193,7,0.05); + padding: 8px 12px; + border-radius: 8px; + } + .partner-code-value { + font-size: 1.2em; + font-weight: 800; + color: var(--brand-yellow); + letter-spacing: 1px; + } + .copy-btn { + background-color: var(--button-bg); + color: var(--button-text); + border: none; + padding: 6px 12px; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + font-size: 0.9em; + } + .copy-btn:active { + opacity: 0.8; + } + + .activate-partner-section { + background-color: var(--card-bg); + border-radius: var(--border-radius); + padding: var(--padding-m); + display: flex; + flex-direction: column; + gap: 12px; + } + .activate-partner-section h3 { + font-size: 1.2em; + font-weight: 700; + margin-bottom: 4px; + } + .activate-form { + display: flex; + gap: 10px; + } + .activate-form input[type="text"] { + flex-grow: 1; + padding: 10px; + border-radius: 8px; + border: 1px solid rgba(255,255,255,0.1); + background-color: rgba(0,0,0,0.2); + color: var(--text-color); + font-family: var(--font-family); + font-size: 1em; + } + .activate-form input[type="text"]::placeholder { + color: var(--text-secondary-color); + } + .activate-form button { + padding: 10px 15px; + border: none; + border-radius: 8px; + background-color: var(--button-bg); + color: var(--button-text); + font-family: var(--font-family); + font-weight: 600; + cursor: pointer; + } + .activate-status { + font-size: 0.9em; + color: var(--text-secondary-color); + min-height: 1.2em; + } + + .history-section, .invoices-section, .business-card-section { background-color: var(--card-bg); border-radius: var(--border-radius); @@ -385,14 +593,20 @@ TEMPLATE = """ .history-description, .invoice-description { font-size: 1em; font-weight: 500; } .history-date, .invoice-date { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; } .history-amount, .invoice-amount { font-size: 1.1em; font-weight: 700; } - .history-amount.accrual { color: #4CAF50; } - .history-amount.deduction { color: #F44336; } + .history-amount.accrual { color: #4CAF50; } /* Bonus accrual */ + .history-amount.deduction { color: #F44336; } /* Bonus deduction */ + .history-amount.referral-accrual { color: #2196F3; } /* Referral bonus accrual */ + .history-amount.debt-accrual { color: #F44336; } /* Debt increase */ + .history-amount.debt-payment { color: #4CAF50; } /* Debt decrease (payment) */ + .invoice-amount { color: var(--brand-yellow); } .no-history, .no-invoices { text-align: center; color: var(--text-secondary-color); padding: 2rem 0; } + + /* Business Card Styles */ .business-card-item { margin-bottom: 10px; } @@ -440,6 +654,8 @@ TEMPLATE = """ height: 20px; width: 20px; } + + /* Invoice Detail Modal */ .modal { display: none; position: fixed; @@ -555,30 +771,38 @@ TEMPLATE = """

{{ "%.2f"|format(user.debts|float) }}

- -
+ +

Ваш ID клиента

{{ user.id }}

- -
-

Ваш реферальный код

-
-

{{ user.referral_code }}

- -
-
- - {% if not user.referred_by_user_id %} -
-

Активировать код

-
- - + +
+

Ваш партнерский код

+
+ {{ user.partner_code | default('Нет кода') }} + {% if user.partner_code %} + + {% endif %}
-

- {% endif %} + +
+

Вас пригласил

+

{{ user.referred_by | default('Никто') }}

+
+ + {% if not user.referred_by %} +
+

Активировать партнерский код

+
+ + +
+

+
+ {% endif %} +

История операций

@@ -591,11 +815,11 @@ TEMPLATE = """ {{ item.date_str }}
{% if item.transaction_type == 'bonus' %} - - {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }} + + {{ '+' if item.type in ['accrual', 'referral'] else '-' }}{{ "%.2f"|format(item.amount|float) }} {% elif item.transaction_type == 'debt' %} - + {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }} {% endif %} @@ -644,7 +868,7 @@ TEMPLATE = """ {% for phone in org_details.phone_numbers %}
  • - + {{ phone }}
  • @@ -663,7 +887,7 @@ TEMPLATE = """
    {% if org_details.whatsapp_link %} - + {{ org_details.whatsapp_link }} {% else %} @@ -676,7 +900,7 @@ TEMPLATE = """
    {% if org_details.telegram_link %} - + {{ org_details.telegram_link }} {% else %} @@ -691,11 +915,13 @@ TEMPLATE = """
    +