diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -7,12 +7,13 @@ import hashlib import json from urllib.parse import unquote, parse_qs, quote import time -from datetime import datetime, timezone, timedelta +from datetime import datetime import logging import threading import random from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError +import pytz BOT_TOKEN = os.getenv("BOT_TOKEN", "7835463659:AAGNePbelZIAOeaglyQi1qulOqnjs4BGQn4") HOST = '0.0.0.0' @@ -24,6 +25,8 @@ HF_DATA_FILE_PATH = "data.json" HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") +BISHKEK_TZ = pytz.timezone('Asia/Bishkek') + app = Flask(__name__) logging.basicConfig(level=logging.INFO) app.secret_key = os.urandom(24) @@ -31,15 +34,25 @@ app.secret_key = os.urandom(24) _data_lock = threading.Lock() visitor_data_cache = {} -def get_bishkek_time(): - return datetime.now(timezone(timedelta(hours=6))) +DEFAULT_ORGANIZATION_INFO = { + "name": "Название вашей организации", + "phones": ["+996000123456"], + "address": "г. Бишкек, ул. Примерная, д.1", + "links": [{"label": "Наш сайт", "url": "https://example.com"}] +} -def generate_unique_id(all_data): +def generate_unique_id(all_data_keys): while True: new_id = str(random.randint(10000, 99999)) - if new_id not in all_data: + if new_id not in all_data_keys: return new_id +def generate_invoice_id(): + return f"INV-{int(time.time() * 1000)}-{random.randint(100, 999)}" + +def get_current_time_bishkek(): + return datetime.now(BISHKEK_TZ) + def download_data_from_hf(): global visitor_data_cache if not HF_TOKEN_READ: @@ -73,35 +86,38 @@ def download_data_from_hf(): logging.error(f"Error downloading data from Hugging Face: {e}") return False -def load_visitor_data(): +def load_data(): global visitor_data_cache with _data_lock: if not visitor_data_cache: try: with open(DATA_FILE, 'r', encoding='utf-8') as f: visitor_data_cache = json.load(f) - logging.info("Visitor data loaded from local JSON.") + logging.info("Data loaded from local JSON.") except FileNotFoundError: - logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.") - visitor_data_cache = {} + logging.warning(f"{DATA_FILE} not found locally. Initializing with default structure.") + visitor_data_cache = {"organization_info": DEFAULT_ORGANIZATION_INFO} except json.JSONDecodeError: - logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.") - visitor_data_cache = {} + logging.error(f"Error decoding {DATA_FILE}. Initializing with default structure.") + visitor_data_cache = {"organization_info": DEFAULT_ORGANIZATION_INFO} except Exception as e: - logging.error(f"Unexpected error loading visitor data: {e}") - visitor_data_cache = {} + logging.error(f"Unexpected error loading data: {e}") + visitor_data_cache = {"organization_info": DEFAULT_ORGANIZATION_INFO} + + if 'organization_info' not in visitor_data_cache: + visitor_data_cache['organization_info'] = DEFAULT_ORGANIZATION_INFO + return visitor_data_cache -def save_visitor_data(data): +def save_data_sync(): with _data_lock: 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"Visitor data successfully saved to {DATA_FILE}.") + logging.info(f"Data successfully saved to {DATA_FILE}.") upload_data_to_hf_async() except Exception as e: - logging.error(f"Error saving visitor data: {e}") + logging.error(f"Error saving data: {e}") def upload_data_to_hf(): if not HF_TOKEN_WRITE: @@ -126,9 +142,9 @@ def upload_data_to_hf(): repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Update bonus data {get_bishkek_time().strftime('%Y-%m-%d %H:%M:%S')}" + commit_message=f"Update data {get_current_time_bishkek().strftime('%Y-%m-%d %H:%M:%S')}" ) - logging.info("Bonus data successfully uploaded to Hugging Face.") + logging.info("Data successfully uploaded to Hugging Face.") except Exception as e: logging.error(f"Error uploading data to Hugging Face: {e}") @@ -164,7 +180,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: + if current_time - auth_date > 86400: # 24 hours logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}).") return parsed_data, True else: @@ -214,152 +230,60 @@ 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; - } - .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 { - 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; - } - .history-section { - background-color: var(--card-bg); - border-radius: var(--border-radius); - padding: var(--padding-l); - } - .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: #4CAF50; } - .history-amount.deduction { color: #F44336; } - .no-history { - text-align: center; - color: var(--text-secondary-color); - padding: 2rem 0; - } - .action-buttons { - display: flex; - gap: var(--padding-m); - margin-top: var(--padding-m); - } - .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; - cursor: pointer; - flex: 1; - text-align: center; - text-decoration: none; - transition: background-color 0.2s, transform 0.2s; - } - .btn-action:hover { - background-color: #e0a800; - transform: translateY(-2px); - } + .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); } + .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; } + + .navigation-tabs { display: flex; gap: 8px; margin-bottom: var(--padding-m); } + .navigation-tabs button { + flex-grow: 1; padding: 12px 10px; font-size: 0.9em; font-weight: 600; color: var(--text-secondary-color); + background-color: var(--card-bg); border: 1px solid rgba(255,255,255,0.1); border-radius: 10px; cursor: pointer; transition: all 0.2s ease; + } + .navigation-tabs button.active { color: var(--brand-black); background-color: var(--brand-yellow); border-color: var(--brand-yellow); } + .content-pane { background-color: var(--card-bg); border-radius: var(--border-radius); padding: var(--padding-l); display: none; } + .content-pane.active { display: block; } + .pane-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; } + .list-item-details { display: flex; flex-direction: column; } + .list-item-description { font-size: 1em; font-weight: 500; } + .list-item-subtext { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; } + .list-item-amount { font-size: 1.1em; font-weight: 700; } + .amount-accrual { color: #4CAF50; } + .amount-deduction { color: #F44336; } + .no-items { text-align: center; color: var(--text-secondary-color); padding: 2rem 0; } + + .biz-card-item { margin-bottom: 12px; } + .biz-card-label { font-size: 0.9em; color: var(--text-secondary-color); margin-bottom: 4px; } + .biz-card-value { font-size: 1.1em; font-weight: 500; } + .biz-card-phone-actions a { margin-right: 10px; color: var(--brand-yellow); text-decoration: none; font-size: 0.9em;} + .biz-card-link a { color: var(--brand-yellow); text-decoration: none; } + + .invoice-item-details { cursor: pointer; } + .invoice-details-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.7); backdrop-filter: blur(5px); } + .invoice-modal-content { background-color: var(--card-bg); margin: 15% auto; padding: var(--padding-l); border-radius: var(--border-radius); width: 90%; max-width: 500px; box-shadow: 0 5px 25px rgba(0,0,0,0.3); } + .invoice-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); } + .invoice-modal-title { font-size: 1.3em; font-weight: 700; } + .invoice-modal-close { font-size: 1.8em; font-weight: bold; cursor: pointer; color: var(--text-secondary-color); } + .invoice-modal-body table { width: 100%; border-collapse: collapse; } + .invoice-modal-body th, .invoice-modal-body td { text-align: left; padding: 8px 4px; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 0.95em; } + .invoice-modal-body th { font-weight: 600; color: var(--text-secondary-color); } + .invoice-modal-body .item-qty, .invoice-modal-body .item-price, .invoice-modal-body .item-total { text-align: right; } + .invoice-grand-total { text-align: right; font-size: 1.1em; font-weight: 700; margin-top: var(--padding-m); padding-top: var(--padding-m); border-top: 1px solid rgba(255,255,255,0.1); }
@@ -385,27 +309,28 @@ TEMPLATE = """{{ user.id }}
-