diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -1,64 +1,39 @@
#!/usr/bin/env python3
import os
-from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for, session
+from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for
import hmac
import hashlib
import json
from urllib.parse import unquote, parse_qs, quote
import time
-from datetime import datetime, timezone
+from datetime import datetime, timezone, timedelta
import logging
import threading
import random
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError
-import pytz # For timezone handling
BOT_TOKEN = os.getenv("BOT_TOKEN", "7835463659:AAGNePbelZIAOeaglyQi1qulOqnjs4BGQn4")
HOST = '0.0.0.0'
PORT = 7860
DATA_FILE = 'data.json'
-ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD") # For admin authentication
REPO_ID = "flpolprojects/examplebonus"
HF_DATA_FILE_PATH = "data.json"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
-APP_TIMEZONE = 'Asia/Bishkek' # Set the desired timezone
-
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
-app.secret_key = os.urandom(24) # Required for session management
+app.secret_key = os.urandom(24)
_data_lock = threading.Lock()
visitor_data_cache = {}
-# --- Timezone Helper ---
-def get_current_time_in_app_timezone():
- return datetime.now(pytz.timezone(APP_TIMEZONE))
-
-def format_time_in_app_timezone(dt_obj, fmt='%Y-%m-%d %H:%M:%S'):
- if dt_obj:
- try:
- if isinstance(dt_obj, float): # Assuming timestamp
- dt_obj = datetime.fromtimestamp(dt_obj, tz=pytz.timezone(APP_TIMEZONE))
- elif isinstance(dt_obj, str): # Assuming ISO format
- dt_obj = datetime.fromisoformat(dt_obj).astimezone(pytz.timezone(APP_TIMEZONE))
- elif isinstance(dt_obj, datetime):
- if dt_obj.tzinfo is None:
- dt_obj = dt_obj.replace(tzinfo=pytz.timezone(APP_TIMEZONE))
- else:
- dt_obj = dt_obj.astimezone(pytz.timezone(APP_TIMEZONE))
-
- return dt_obj.strftime(fmt)
- except Exception as e:
- logging.error(f"Error formatting time '{dt_obj}': {e}")
- return str(dt_obj) # Fallback
- return ""
+def get_bishkek_time():
+ return datetime.now(timezone(timedelta(hours=6)))
-# --- Data Management ---
def generate_unique_id(all_data):
while True:
new_id = str(random.randint(10000, 99999))
@@ -106,21 +81,6 @@ def load_visitor_data():
with open(DATA_FILE, 'r', encoding='utf-8') as f:
visitor_data_cache = json.load(f)
logging.info("Visitor data loaded from local JSON.")
- # Ensure all timestamps are timezone-aware (or converted to a consistent format)
- for user_id, user_data in visitor_data_cache.items():
- if 'visited_at' in user_data and isinstance(user_data['visited_at'], (int, float)):
- user_data['visited_at'] = datetime.fromtimestamp(user_data['visited_at'], tz=pytz.timezone(APP_TIMEZONE))
- if 'history' in user_data:
- for item in user_data['history']:
- if 'date' in item and isinstance(item['date'], str):
- item['date'] = datetime.fromisoformat(item['date']).astimezone(pytz.timezone(APP_TIMEZONE))
- item['date_str'] = format_time_in_app_timezone(item['date'])
- if 'debt_history' in user_data:
- for item in user_data['debt_history']:
- if 'date' in item and isinstance(item['date'], str):
- item['date'] = datetime.fromisoformat(item['date']).astimezone(pytz.timezone(APP_TIMEZONE))
- item['date_str'] = format_time_in_app_timezone(item['date'])
-
except FileNotFoundError:
logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.")
visitor_data_cache = {}
@@ -136,24 +96,8 @@ def save_visitor_data(data):
with _data_lock:
try:
visitor_data_cache.update(data)
- # Before saving, ensure all datetime objects are converted to ISO strings
- data_to_save = {}
- for user_id, user_data in visitor_data_cache.items():
- user_data_copy = user_data.copy()
- if 'visited_at' in user_data_copy and isinstance(user_data_copy['visited_at'], datetime):
- user_data_copy['visited_at'] = user_data_copy['visited_at'].isoformat()
- if 'history' in user_data_copy:
- for item in user_data_copy['history']:
- if 'date' in item and isinstance(item['date'], datetime):
- item['date'] = item['date'].isoformat()
- if 'debt_history' in user_data_copy:
- for item in user_data_copy['debt_history']:
- if 'date' in item and isinstance(item['date'], datetime):
- item['date'] = item['date'].isoformat()
- data_to_save[user_id] = user_data_copy
-
with open(DATA_FILE, 'w', encoding='utf-8') as f:
- json.dump(data_to_save, f, ensure_ascii=False, indent=4)
+ json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
upload_data_to_hf_async()
except Exception as e:
@@ -182,7 +126,7 @@ def upload_data_to_hf():
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
- commit_message=f"Update bonus data {get_current_time_in_app_timezone().strftime('%Y-%m-%d %H:%M:%S')}"
+ commit_message=f"Update bonus data {get_bishkek_time().strftime('%Y-%m-%d %H:%M:%S')}"
)
logging.info("Bonus data successfully uploaded to Hugging Face.")
except Exception as e:
@@ -230,9 +174,7 @@ def verify_telegram_data(init_data_str):
logging.error(f"Error verifying Telegram data: {e}")
return None, False
-# --- Templates ---
-
-USER_TEMPLATE = """
+TEMPLATE = """
@@ -259,35 +201,22 @@ USER_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);
- --nav-button-color: var(--brand-black);
- --nav-button-text-color: var(--text-color);
- --nav-button-active-color: var(--brand-yellow);
- --nav-button-active-text-color: var(--brand-black);
}
- body {
+ * { box-sizing: border-box; margin: 0; padding: 0; }
+ html, body {
background-color: var(--brand-black);
font-family: var(--font-family);
color: var(--text-color);
- padding: 0; /* Remove default body padding */
+ padding: var(--padding-m);
overscroll-behavior-y: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
visibility: hidden;
min-height: 100vh;
- display: flex;
- flex-direction: column;
- }
- .main-content {
- flex-grow: 1;
- padding: var(--padding-m);
- display: flex;
- flex-direction: column;
- gap: var(--padding-m);
}
.container {
max-width: 600px;
margin: 0 auto;
- width: 100%;
display: flex;
flex-direction: column;
gap: var(--padding-m);
@@ -403,373 +332,113 @@ USER_TEMPLATE = """
.history-amount { font-size: 1.1em; font-weight: 700; }
.history-amount.accrual { color: #4CAF50; }
.history-amount.deduction { color: #F44336; }
-
- /* Navigation */
- .nav-bar {
- display: flex;
- justify-content: space-around;
- padding: 10px var(--padding-m);
- background-color: var(--card-bg);
- border-top: 1px solid rgba(255, 255, 255, 0.1);
- position: sticky;
- bottom: 0;
- left: 0;
- width: 100%;
- z-index: 10;
- }
- .nav-button {
- background-color: var(--nav-button-color);
- color: var(--nav-button-text-color);
- padding: 12px 20px;
- border-radius: 12px;
- font-weight: 600;
- text-decoration: none;
- flex: 1;
- text-align: center;
- margin: 0 5px;
- border: none;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- }
- .nav-button.active {
- background-color: var(--nav-button-active-color);
- color: var(--nav-button-active-text-color);
- }
- .nav-button i { font-size: 1.2em; }
-
- /* Modal Styles for Invoices */
- .modal {
- display: none;
- position: fixed;
- z-index: 1050;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- overflow: auto;
- background-color: rgba(0,0,0,0.6);
- backdrop-filter: blur(5px);
- font-family: var(--font-family);
- }
- .modal-dialog {
- max-width: 600px;
- margin: 20px auto;
- background-color: var(--card-bg);
- border-radius: var(--border-radius);
- box-shadow: 0 8px 30px rgba(0,0,0,0.2);
- overflow: hidden;
- }
- .modal-content {
- padding: var(--padding-l);
- }
- .modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 1px solid rgba(255,255,255,0.1);
- padding-bottom: var(--padding-m);
- margin-bottom: var(--padding-m);
- }
- .modal-title {
- font-size: 1.5em;
- font-weight: 700;
- color: var(--brand-yellow);
- }
- .modal-close {
- background: none;
- border: none;
- font-size: 2em;
- color: var(--text-secondary-color);
- cursor: pointer;
- padding: 0 10px;
- }
- .modal-body {
- margin-bottom: var(--padding-m);
- }
- .invoice-item {
- display: grid;
- grid-template-columns: 1fr 60px 100px 100px;
- gap: 10px;
- align-items: center;
- padding: 10px 0;
- border-bottom: 1px solid rgba(255,255,255,0.05);
- }
- .invoice-item:last-child { border-bottom: none; }
- .item-name { font-size: 0.95em; }
- .item-quantity, .item-price, .item-total { font-weight: 500; text-align: right; }
- .invoice-summary {
- text-align: right;
- margin-top: var(--padding-m);
- padding-top: var(--padding-m);
- border-top: 1px solid rgba(255,255,255,0.1);
- }
- .invoice-total-label { color: var(--text-secondary-color); }
- .invoice-total-amount {
- font-size: 1.8em;
- font-weight: 800;
- color: var(--brand-yellow);
- }
- .invoice-list {
- list-style: none;
- padding: 0;
- }
- .invoice-item-summary {
- display: flex;
- justify-content: space-between;
- padding: 10px 0;
- border-bottom: 1px solid rgba(255,255,255,0.05);
- font-size: 0.9em;
- }
- .invoice-item-summary:last-child { border-bottom: none; }
- .invoice-list-placeholder {
+ .no-history {
text-align: center;
color: var(--text-secondary-color);
padding: 2rem 0;
}
- .visit-card-section {
- background-color: var(--card-bg);
- border-radius: var(--border-radius);
- padding: var(--padding-l);
+ .action-buttons {
+ display: flex;
+ gap: var(--padding-m);
margin-top: var(--padding-m);
}
- .visit-card-title {
- font-size: 1.4em;
+ .btn-action {
+ background-color: var(--brand-yellow);
+ color: var(--brand-black);
+ padding: 12px 20px;
+ border: none;
+ border-radius: 12px;
+ font-size: 1.1em;
font-weight: 700;
- margin-bottom: var(--padding-m);
- padding-bottom: var(--padding-m);
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
- }
- .visit-card-item {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
- gap: 12px;
- }
- .visit-card-item .icon {
- color: var(--brand-yellow);
- font-size: 1.3em;
- min-width: 24px;
+ cursor: pointer;
+ flex: 1;
text-align: center;
- }
- .visit-card-item .label {
- font-size: 1.1em;
- font-weight: 500;
- color: var(--text-secondary-color);
- flex-shrink: 0;
- }
- .visit-card-item .value {
- font-size: 1.1em;
- font-weight: 500;
- word-break: break-all;
- }
- .visit-card-item .phone-link,
- .visit-card-item .social-link {
text-decoration: none;
- color: var(--text-color);
- font-weight: 500;
- font-size: 1.1em;
- display: flex;
- align-items: center;
- gap: 8px;
+ transition: background-color 0.2s, transform 0.2s;
+ }
+ .btn-action:hover {
+ background-color: #e0a800;
+ transform: translateY(-2px);
}
- .visit-card-item .social-link { color: var(--brand-yellow); }
-
-
-
-
- {% if current_view == 'history' %}
-
-
-
Ваши бонусы
-
{{ "%.2f"|format(user.bonuses|float) }}
-
-
-
Ваш долг
-
{{ "%.2f"|format(user.debts|float) }}
-
-
-
-
- Ваш ID клиента
- {{ user.id }}
-
-
-
- История операций
- {% if user.combined_history %}
-
- {% for item in user.combined_history %}
- -
-
- {{ item.description }}
- {{ item.date_str }}
-
- {% 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 %}
-
- {% else %}
- Операций пока не было.
- {% endif %}
-
- {% elif current_view == 'invoices' %}
-
- Мои накладные
- {% if user.invoices %}
-
- {% for invoice in user.invoices|sort(attribute='date_created', reverse=True) %}
- -
- Накладная #{{ invoice.id }}
- {{ invoice.date_str }}
- {{ "%.2f"|format(invoice.total_amount|float) }}
-
-
- {% endfor %}
-
- {% else %}
- У вас пока нет накладных.
- {% endif %}
-
- {% elif current_view == 'visit' %}
-
- Визитка нашей компании
- {% if company_data %}
- {% if company_data.name %}
-
- Компания:
- {{ company_data.name }}
-
- {% endif %}
- {% if company_data.phones %}
- {% for phone in company_data.phones %}
-
- {% endfor %}
- {% endif %}
- {% if company_data.address %}
-
- 📍
- Адрес:
- {{ company_data.address }}
-
- {% endif %}
- {% if company_data.social_links %}
- {% for link in company_data.social_links %}
-
- {% endfor %}
- {% endif %}
- {% else %}
- Визитные данные еще не добавлены.
- {% endif %}
-
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
Ваши бонусы
+
{{ "%.2f"|format(user.bonuses|float) }}
+
+
Ваш долг
+
{{ "%.2f"|format(user.debts|float) }}
+
+
+
+
+ Ваш ID клиента
+ {{ user.id }}
+
+
+
+
+
+ История операций
+ {% if user.combined_history %}
+
+ {% for item in user.combined_history %}
+ -
+
+ {{ item.description }}
+ {{ item.date_str }}
+
+ {% 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 %}
+
+ {% else %}
+ Операций пока не было.
+ {% endif %}
+
"""
-ADMIN_TEMPLATE = """
+COMPANY_INFO_TEMPLATE = """
-
-
Bonus Admin
+
+
Визитка Компании
+
-
+
+
+
+
+
+
+
+
+
+
+ {% if company_info.website %}
+
+ {% endif %}
+ {% if company_info.facebook %}
+
+ {% endif %}
+ {% if company_info.instagram %}
+
+ {% endif %}
+ {% if company_info.tiktok %}
+
+ {% endif %}
+
+
+
Назад
+
+
+
+
+
+"""
+
+INVOICE_LIST_TEMPLATE = """
+
+
+
+
+
+
Мои накладные
+
+
+
+
+
+
+
+
+
+
+ {% if invoices %}
+
+ {% for invoice in invoices %}
+ -
+
+
+ {% for item in invoice.items %}
+ {{ item.product_name }} - {{ item.quantity }} шт. x {{ "%.2f"|format(item.unit_price) }} = {{ "%.2f"|format(item.total_price) }}
+ {% endfor %}
+
+ Итого: {{ "%.2f"|format(invoice.total_amount) }}
+
+ {% endfor %}
+
+ {% else %}
+
Накладных пока нет.
+ {% endif %}
+
+
Назад
+
+
+
+
+
+"""
+
+ADMIN_TEMPLATE = """
+
+
+
+
+
+
Bonus Admin
+
+
+
+
Панель администратора Bonus
-
-
- {% if not admin_logged_in %}
-
-
-
-
-
-
-
-
+
+
+
{{ summary.total_users }}
+
Всего клиентов
+
+
+
{{ "%.2f"|format(summary.total_bonuses|float) }}
+
Всего бонусов
+
+
+
{{ "%.2f"|format(summary.total_debts|float) }}
+
Всего долгов
+
+
+
{{ summary.users_with_debt }}
+
Клиенты с долгом
- {% endif %}
-
-
-
-
{{ summary.total_users }}
-
Всего клиентов
+
+
+
+
+
+
+
Информация о компании
+