diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -7,7 +7,7 @@ import hashlib import json from urllib.parse import unquote, parse_qs, quote import time -from datetime import datetime, timezone, timedelta +from datetime import datetime, timezone import logging import threading import random @@ -31,19 +31,15 @@ app.secret_key = os.urandom(24) _data_lock = threading.Lock() visitor_data_cache = {} -BISHKEK_TZ = timezone(timedelta(hours=6)) +KYRGYZSTAN_TIMEZONE = timezone(timedelta(hours=6)) # UTC+6 for Bishkek -DEFAULT_ORGANIZATION_DETAILS = { - "name": "Название вашей организации", - "phones": [], - "address": "Ваш адрес", - "links": [] -} +def get_current_time_kyrgyzstan(): + return datetime.now(KYRGYZSTAN_TIMEZONE) -def generate_unique_id(existing_ids_dict): +def generate_unique_id(all_data): while True: new_id = str(random.randint(10000, 99999)) - if new_id not in existing_ids_dict: + if new_id not in all_data: return new_id def download_data_from_hf(): @@ -67,14 +63,11 @@ def download_data_from_hf(): with _data_lock: try: with open(DATA_FILE, 'r', encoding='utf-8') as f: - loaded_data = json.load(f) - visitor_data_cache = loaded_data - if '_organization_details' not in visitor_data_cache: - visitor_data_cache['_organization_details'] = DEFAULT_ORGANIZATION_DETAILS.copy() + visitor_data_cache = json.load(f) logging.info("Successfully loaded downloaded data into cache.") except (FileNotFoundError, json.JSONDecodeError) as e: - logging.error(f"Error reading downloaded data file: {e}. Initializing cache.") - visitor_data_cache = {'_organization_details': DEFAULT_ORGANIZATION_DETAILS.copy()} + logging.error(f"Error reading downloaded data file: {e}. Starting with empty cache.") + visitor_data_cache = {} return True except RepositoryNotFoundError: logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.") @@ -89,32 +82,28 @@ def load_visitor_data(): try: with open(DATA_FILE, 'r', encoding='utf-8') as f: visitor_data_cache = json.load(f) - if '_organization_details' not in visitor_data_cache: - visitor_data_cache['_organization_details'] = DEFAULT_ORGANIZATION_DETAILS.copy() logging.info("Visitor data loaded from local JSON.") except FileNotFoundError: logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.") - visitor_data_cache = {'_organization_details': DEFAULT_ORGANIZATION_DETAILS.copy()} + visitor_data_cache = {} except json.JSONDecodeError: logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.") - visitor_data_cache = {'_organization_details': DEFAULT_ORGANIZATION_DETAILS.copy()} + visitor_data_cache = {} except Exception as e: logging.error(f"Unexpected error loading visitor data: {e}") - visitor_data_cache = {'_organization_details': DEFAULT_ORGANIZATION_DETAILS.copy()} + visitor_data_cache = {} return visitor_data_cache -def save_data_locally_and_upload_async(data_to_update=None): +def save_visitor_data(data): with _data_lock: - if data_to_update: - for key, value in data_to_update.items(): - visitor_data_cache[key] = value try: + visitor_data_cache.update(data) with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4) - logging.info(f"Data successfully saved to {DATA_FILE}.") + logging.info(f"Visitor data successfully saved to {DATA_FILE}.") upload_data_to_hf_async() except Exception as e: - logging.error(f"Error saving data: {e}") + logging.error(f"Error saving visitor data: {e}") def upload_data_to_hf(): if not HF_TOKEN_WRITE: @@ -139,7 +128,7 @@ def upload_data_to_hf(): repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Update bonus data {datetime.now(BISHKEK_TZ).strftime('%Y-%m-%d %H:%M:%S')}" + commit_message=f"Update bonus data {get_current_time_kyrgyzstan().strftime('%Y-%m-%d %H:%M:%S')}" ) logging.info("Bonus data successfully uploaded to Hugging Face.") except Exception as e: @@ -177,7 +166,7 @@ def verify_telegram_data(init_data_str): if calculated_hash == received_hash: auth_date = int(parsed_data.get('auth_date', [0])[0]) current_time = int(time.time()) - if current_time - auth_date > 86400: # 24 hours + if current_time - auth_date > 86400: logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}).") return parsed_data, True else: @@ -203,6 +192,7 @@ TEMPLATE = """ --brand-yellow: #FFC107; --brand-black: #101010; --brand-red: #F44336; + --brand-green: #4CAF50; --card-bg: #1c1c1e; --text-color: #ffffff; --text-secondary-color: #a0a0a0; @@ -227,63 +217,257 @@ TEMPLATE = """ visibility: hidden; min-height: 100vh; } - .container { max-width: 600px; margin: 0 auto; display: flex; flex-direction: column; gap: var(--padding-m); } - .header { text-align: left; padding: var(--padding-m) 0; } - .logo { font-size: 2.5em; font-weight: 800; color: var(--text-color); letter-spacing: -1px; } + .container { + max-width: 600px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: var(--padding-m); + } + .header { + text-align: left; + padding: var(--padding-m) 0; + } + .logo { + font-size: 2.5em; + font-weight: 800; + color: var(--text-color); + letter-spacing: -1px; + } .logo span { color: var(--brand-yellow); } - .welcome-text { font-size: 1em; color: var(--text-secondary-color); margin-top: 4px; } - - .tabs { display: flex; gap: 8px; margin-bottom: var(--padding-m); border-bottom: 1px solid var(--card-bg); } - .tab-button { - padding: 10px 15px; background-color: transparent; color: var(--text-secondary-color); - border: none; border-bottom: 3px solid transparent; cursor: pointer; font-size: 1em; font-weight: 600; - transition: color 0.3s, border-color 0.3s; + .welcome-text { + font-size: 1em; + color: var(--text-secondary-color); + margin-top: 4px; + } + .card-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--padding-m); } - .tab-button.active { color: var(--brand-yellow); border-bottom-color: var(--brand-yellow); } - .tab-content { display: none; flex-direction: column; gap: var(--padding-m); } - .tab-content.active { display: flex; } - - .card-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--padding-m); } .bonus-card, .debt-card { background: linear-gradient(145deg, #2a2a2a, #1c1c1c); - border-radius: calc(var(--border-radius) + 8px); padding: var(--padding-l); text-align: center; - position: relative; overflow: hidden; - } - .bonus-card { box-shadow: var(--shadow-glow); border: 1px solid rgba(255, 193, 7, 0.2); } - .debt-card { box-shadow: var(--shadow-glow-red); border: 1px solid rgba(244, 67, 54, 0.2); } - .card-label { font-size: 1.1em; font-weight: 500; color: var(--text-secondary-color); margin-bottom: 12px; } - .bonus-amount, .debt-amount { font-size: 3em; font-weight: 800; letter-spacing: -2px; line-height: 1; } - .bonus-amount { color: var(--brand-yellow); } - .debt-amount { color: var(--brand-red); } - - .client-id-card { background-color: var(--card-bg); border-radius: var(--border-radius); padding: var(--padding-m); display: flex; justify-content: space-between; align-items: center; } - .client-id-label { font-weight: 500; color: var(--text-secondary-color); } - .client-id-value { font-size: 1.3em; font-weight: 700; color: var(--brand-yellow); letter-spacing: 2px; background-color: rgba(255,193,7,0.1); padding: 4px 10px; border-radius: 8px; } - - .section-card { background-color: var(--card-bg); border-radius: var(--border-radius); padding: var(--padding-l); } - .section-title { font-size: 1.4em; font-weight: 700; margin-bottom: var(--padding-m); padding-bottom: var(--padding-m); border-bottom: 1px solid rgba(255, 255, 255, 0.1); } - - .list { list-style: none; padding: 0; margin: 0; max-height: 40vh; overflow-y: auto; } - .list-item { display: flex; justify-content: space-between; align-items: center; padding: 14px 4px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); } - .list-item:last-child { border-bottom: none; } - .item-details { display: flex; flex-direction: column; } - .item-description { font-size: 1em; font-weight: 500; } - .item-date { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; } - .item-amount { font-size: 1.1em; font-weight: 700; } - .item-amount.accrual { color: #4CAF50; } - .item-amount.deduction { color: #F44336; } - .no-items { text-align: center; color: var(--text-secondary-color); padding: 2rem 0; } - - .visiting-card-item { margin-bottom: 12px; } - .visiting-card-item strong { color: var(--brand-yellow); display: block; margin-bottom: 4px; font-size: 0.9em; } - .visiting-card-item span, .visiting-card-item a { color: var(--text-color); text-decoration: none; font-size: 1.1em; } - .visiting-card-item a:hover { text-decoration: underline; } - .phone-actions button { - background-color: rgba(255, 193, 7, 0.2); color: var(--brand-yellow); - border: none; padding: 8px 12px; margin-right: 8px; margin-top: 4px; border-radius: 8px; cursor: pointer; - } - .invoice-item-details { background-color: rgba(255,255,255,0.05); padding: 10px; margin-top: 10px; border-radius: 8px;} - .invoice-item-product { display: flex; justify-content: space-between; font-size: 0.9em; margin-bottom: 5px; } + border-radius: calc(var(--border-radius) + 8px); + padding: var(--padding-l); + text-align: center; + position: relative; + overflow: hidden; + } + .bonus-card { + box-shadow: var(--shadow-glow); + border: 1px solid rgba(255, 193, 7, 0.2); + } + .debt-card { + box-shadow: var(--shadow-glow-red); + border: 1px solid rgba(244, 67, 54, 0.2); + } + .card-label { + font-size: 1.1em; + font-weight: 500; + color: var(--text-secondary-color); + margin-bottom: 12px; + } + .bonus-amount { + font-size: 3em; + font-weight: 800; + color: var(--brand-yellow); + letter-spacing: -2px; + line-height: 1; + } + .debt-amount { + font-size: 3em; + font-weight: 800; + color: var(--brand-red); + letter-spacing: -2px; + line-height: 1; + } + .client-id-card { + background-color: var(--card-bg); + border-radius: var(--border-radius); + padding: var(--padding-m); + display: flex; + justify-content: space-between; + align-items: center; + } + .client-id-label { + font-weight: 500; + color: var(--text-secondary-color); + } + .client-id-value { + font-size: 1.3em; + font-weight: 700; + color: var(--brand-yellow); + letter-spacing: 2px; + background-color: rgba(255,193,7,0.1); + padding: 4px 10px; + border-radius: 8px; + } + .navigation-buttons { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--padding-m); + margin-top: var(--padding-m); + } + .nav-button { + background-color: var(--card-bg); + border-radius: var(--border-radius); + padding: var(--padding-m); + text-align: center; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.2s ease; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 80px; + border: 1px solid rgba(255, 255, 255, 0.05); + } + .nav-button:hover { + background-color: #2a2a2c; + transform: translateY(-3px); + } + .nav-button-icon { + font-size: 1.8em; + margin-bottom: 8px; + color: var(--brand-yellow); + } + .nav-button-text { + font-size: 0.9em; + font-weight: 500; + color: var(--text-secondary-color); + } + .history-section { + background-color: var(--card-bg); + border-radius: var(--border-radius); + padding: var(--padding-l); + margin-top: var(--padding-m); + } + .history-title { + font-size: 1.4em; + font-weight: 700; + margin-bottom: var(--padding-m); + padding-bottom: var(--padding-m); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + .history-list { + list-style: none; + padding: 0; + margin: 0; + max-height: 35vh; + overflow-y: auto; + } + .history-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 4px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + } + .history-item:last-child { border-bottom: none; } + .history-details { display: flex; flex-direction: column; } + .history-description { font-size: 1em; font-weight: 500; } + .history-date { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; } + .history-amount { font-size: 1.1em; font-weight: 700; } + .history-amount.accrual { color: var(--brand-green); } + .history-amount.deduction { color: var(--brand-red); } + .no-history { + text-align: center; + color: var(--text-secondary-color); + padding: 2rem 0; + } + .modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0,0,0,0.5); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + padding: 20px; + box-sizing: border-box; + } + .modal-content { + background-color: var(--card-bg); + margin: 20px auto; + padding: var(--padding-l); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: var(--border-radius); + width: 100%; + max-width: 700px; + box-shadow: 0 0 30px rgba(255, 193, 7, 0.1); + } + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--padding-m); + padding-bottom: var(--padding-m); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + .modal-header h2 { + font-size: 1.5em; + font-weight: 700; + color: var(--brand-yellow); + } + .modal-close { + font-size: 2em; + cursor: pointer; + color: var(--text-secondary-color); + line-height: 1; + } + .modal-close:hover { + color: var(--text-color); + } + .invoice-section { + background-color: var(--card-bg); + border-radius: var(--border-radius); + padding: var(--padding-m); + margin-bottom: var(--padding-m); + border: 1px solid rgba(255, 255, 255, 0.05); + } + .invoice-title { + font-size: 1.3em; + font-weight: 700; + margin-bottom: var(--padding-m); + color: var(--brand-yellow); + display: flex; + justify-content: space-between; + align-items: center; + } + .invoice-date { + font-size: 0.9em; + color: var(--text-secondary-color); + } + .invoice-item { + display: flex; + justify-content: space-between; + padding: 10px 0; + border-bottom: 1px dashed rgba(255, 255, 255, 0.1); + font-size: 0.95em; + } + .invoice-item:last-child { + border-bottom: none; + } + .invoice-item .name { flex-grow: 1; margin-right: 10px; } + .invoice-item .quantity { min-width: 50px; text-align: right; margin-right: 10px; } + .invoice-item .price { min-width: 80px; text-align: right; } + .invoice-total { + text-align: right; + font-size: 1.2em; + font-weight: 700; + margin-top: var(--padding-m); + padding-top: var(--padding-m); + border-top: 1px solid rgba(255, 255, 255, 0.1); + } + .no-invoices { + text-align: center; + color: var(--text-secondary-color); + padding: 2rem 0; + } @@ -293,127 +477,157 @@ TEMPLATE = """

Добро пожаловать!

-
- - - -
+
+
+

Ваши бонусы

+

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

+
+
+

Ваш долг

+

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

+
+
+ +
+

Ваш ID клиента

+

{{ user.id }}

+
+ + + +
+

Последние операции

+ {% if user.combined_history %} + + {% if user.combined_history|length > 5 %} +

Посмотреть всю историю

+ {% endif %} + {% else %} +

Операций пока не было.

+ {% endif %} +
+ -
-
-
-

Ваши бонусы

-

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

-
-
-

Ваш долг

-

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

-
-
- -
-

Ваш ID клиента

-

{{ user.id }}

-
- -
-

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

- {% if user.combined_history %} - - {% else %} -

Операций пока не было.

+ +
+
+ Call + {% if business_info.whatsapp %} + WhatsApp + {% endif %} + {% if business_info.telegram_username %} + Telegram + {% endif %} +
+
+ -
-
-

Мои накладные

- {% if user.invoices %} - - {% else %} -

У вас пока нет накладных.

- {% endif %} -
+
+ Итого: {{ "%.2f"|format(invoice.total_amount|float) }} сом +
+
+ {% endfor %} + {% else %} +

У вас пока нет накладных.

+ {% endif %} + -
-
-

Визитка организации

-
- Название: - {{ organization.name }} -
-
- Адрес: - {{ organization.address }} -
- {% if organization.phones %} -
- Телефоны: - {% for phone in organization.phones %} -
- {{ phone.display_text or phone.number }} -
- - {% if phone.type == 'whatsapp' or phone.type == 'telegram_number' %} - - {% endif %} - {% if phone.type == 'telegram' or phone.type == 'telegram_number' %} - - {% endif %} + + - {% endif %} - {% if organization.links %} -
- Ссылки: - {% for link in organization.links %} -

{{ link.text }}

+ {% if item.transaction_type == 'bonus' %} + + {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }} + + {% elif item.transaction_type == 'debt' %} + + {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }} + + {% endif %} + {% endfor %} -
- {% endif %} -
+ + {% else %} +

Операций пока не было.

+ {% endif %}
@@ -423,6 +637,7 @@ TEMPLATE = """ function applyTheme(themeParams) { const root = document.documentElement; const isDark = themeParams.bg_color ? (parseInt(themeParams.bg_color.substring(1, 3), 16) < 128) : true; + root.style.setProperty('--brand-black', themeParams.bg_color || '#101010'); root.style.setProperty('--text-color', themeParams.text_color || '#ffffff'); root.style.setProperty('--text-secondary-color', themeParams.hint_color || '#a0a0a0'); @@ -436,6 +651,7 @@ TEMPLATE = """ document.body.style.visibility = 'visible'; return; } + tg.ready(); tg.expand(); @@ -485,36 +701,29 @@ TEMPLATE = """ } else { window.addEventListener('load', setupTelegram, {once: true}); setTimeout(() => { - if (document.body.style.visibility !== 'visible') { document.body.style.visibility = 'visible'; } + if (document.body.style.visibility !== 'visible') { + document.body.style.visibility = 'visible'; + } }, 3000); } - function showTab(tabId, Tgelement) { - document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active')); - document.querySelectorAll('.tab-button').forEach(tb => tb.classList.remove('active')); - document.getElementById(tabId + '_content').classList.add('active'); - Tgelement.classList.add('active'); - } - - function toggleInvoiceDetails(invoiceId) { - const detailsElement = document.getElementById(invoiceId); - if (detailsElement) { - detailsElement.style.display = detailsElement.style.display === 'none' ? 'flex' : 'none'; + function openModal(modalId) { + const modal = document.getElementById(modalId); + if (modal) { + modal.style.display = 'block'; } } - function handlePhoneAction(action, number) { - const cleanNumber = number.replace(/\\D/g, ''); - let url = ''; - if (action === 'call') { - url = `tel:${number}`; - } else if (action === 'whatsapp') { - url = `https://wa.me/${cleanNumber}`; - } else if (action === 'telegram_number') { - url = `https://t.me/${cleanNumber.startsWith('+') ? cleanNumber.substring(1) : cleanNumber}`; + function closeModal(modalId) { + const modal = document.getElementById(modalId); + if (modal) { + modal.style.display = 'none'; } - if (url) { - tg.openLink(url); + } + + window.onclick = function(event) { + if (event.target.classList.contains('modal')) { + closeModal(event.target.id); } } @@ -534,31 +743,38 @@ ADMIN_TEMPLATE = """

Панель администратора Bonus

- -
-

Сводка по клиентам

-
-
{{ summary.total_users }}
Всего клиентов
-
{{ "%.2f"|format(summary.total_bonuses|float) }}
Всего бонусов
-
{{ "%.2f"|format(summary.total_debts|float) }}
Всего долгов
-
{{ summary.users_with_debt }}
Клиенты с долгом
+
+
+
{{ summary.total_users }}
+
Всего клиентов
+
+
+
{{ "%.2f"|format(summary.total_bonuses|float) }}
+
Всего бонусов
+
+
+
{{ "%.2f"|format(summary.total_debts|float) }}
+
Всего долгов
+
+
+
{{ summary.users_with_debt }}
+
Клиенты с долгом
-
-

Данные организации (Визитка)

-
-
- - -
-
- - -
- -

Телефоны

-
- {% for phone in organization_details.phones %} -
- - - - -
- {% endfor %} -
- - -

Ссылки

- - - -
- -
-
-
+
+ + +
- -
-

Управление клиентами

-
- - -
- - {% if users %} -
- {% for user in users|sort(attribute='visited_at', reverse=true) %} -
- -