diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,3 +1,4 @@ + #!/usr/bin/env python3 import os @@ -7,12 +8,13 @@ import hashlib import json from urllib.parse import unquote, parse_qs, quote import time -from datetime import datetime, timezone +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,17 +26,15 @@ 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) _data_lock = threading.Lock() visitor_data_cache = {} - -KYRGYZSTAN_TIMEZONE = timezone(timedelta(hours=6)) # UTC+6 for Bishkek - -def get_current_time_kyrgyzstan(): - return datetime.now(KYRGYZSTAN_TIMEZONE) +org_contact_info_cache = {} def generate_unique_id(all_data): while True: @@ -43,7 +43,7 @@ def generate_unique_id(all_data): return new_id def download_data_from_hf(): - global visitor_data_cache + global visitor_data_cache, org_contact_info_cache if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ not set. Skipping Hugging Face download.") return False @@ -63,11 +63,14 @@ def download_data_from_hf(): with _data_lock: try: with open(DATA_FILE, 'r', encoding='utf-8') as f: - visitor_data_cache = json.load(f) + full_data = json.load(f) + visitor_data_cache = full_data.get('users', {}) + org_contact_info_cache = full_data.get('org_contact_info', {}) logging.info("Successfully loaded downloaded data into cache.") except (FileNotFoundError, json.JSONDecodeError) as e: logging.error(f"Error reading downloaded data file: {e}. Starting with empty cache.") visitor_data_cache = {} + org_contact_info_cache = {} return True except RepositoryNotFoundError: logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.") @@ -75,35 +78,60 @@ def download_data_from_hf(): logging.error(f"Error downloading data from Hugging Face: {e}") return False -def load_visitor_data(): - global visitor_data_cache +def load_full_data(): + global visitor_data_cache, org_contact_info_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.") - except FileNotFoundError: - logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.") - visitor_data_cache = {} - except json.JSONDecodeError: - logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.") - visitor_data_cache = {} - except Exception as e: - logging.error(f"Unexpected error loading visitor data: {e}") - visitor_data_cache = {} - return visitor_data_cache + # Check cache first + if visitor_data_cache or org_contact_info_cache: + return {'users': visitor_data_cache, 'org_contact_info': org_contact_info_cache} -def save_visitor_data(data): + try: + with open(DATA_FILE, 'r', encoding='utf-8') as f: + full_data = json.load(f) + visitor_data_cache = full_data.get('users', {}) + org_contact_info_cache = full_data.get('org_contact_info', {}) + logging.info("Full data loaded from local JSON.") + except FileNotFoundError: + logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.") + visitor_data_cache = {} + org_contact_info_cache = {} + except json.JSONDecodeError: + logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.") + visitor_data_cache = {} + org_contact_info_cache = {} + except Exception as e: + logging.error(f"Unexpected error loading full data: {e}") + visitor_data_cache = {} + org_contact_info_cache = {} + + return {'users': visitor_data_cache, 'org_contact_info': org_contact_info_cache} + + +def save_full_data(): with _data_lock: try: - visitor_data_cache.update(data) + full_data_to_save = { + 'users': visitor_data_cache, + 'org_contact_info': org_contact_info_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}.") + json.dump(full_data_to_save, f, ensure_ascii=False, indent=4) + logging.info(f"Full 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 full data: {e}") + + +def save_visitor_data(data): + with _data_lock: + visitor_data_cache.update(data) + save_full_data() + +def save_org_contact_info(info): + global org_contact_info_cache + with _data_lock: + org_contact_info_cache = info + save_full_data() def upload_data_to_hf(): if not HF_TOKEN_WRITE: @@ -128,7 +156,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_kyrgyzstan().strftime('%Y-%m-%d %H:%M:%S')}" + commit_message=f"Update bonus data {datetime.now(BISHKEK_TZ).strftime('%Y-%m-%d %H:%M:%S')}" ) logging.info("Bonus data successfully uploaded to Hugging Face.") except Exception as e: @@ -145,7 +173,7 @@ def periodic_backup(): while True: time.sleep(3600) logging.info("Initiating periodic backup...") - upload_data_to_hf() + save_full_data() # Save locally and trigger HF upload def verify_telegram_data(init_data_str): try: @@ -168,6 +196,8 @@ def verify_telegram_data(init_data_str): current_time = int(time.time()) if current_time - auth_date > 86400: logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}).") + # Decide if you want to fail verification for old data + # return parsed_data, False # Uncomment to fail old data return parsed_data, True else: logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}") @@ -204,6 +234,7 @@ 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); + --section-bg: #2c2c2e; } * { box-sizing: border-box; margin: 0; padding: 0; } html, body { @@ -214,7 +245,7 @@ TEMPLATE = """ overscroll-behavior-y: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - visibility: hidden; + visibility: hidden; /* Hide until theme applied or loaded */ min-height: 100vh; } .container { @@ -261,21 +292,21 @@ TEMPLATE = """ box-shadow: var(--shadow-glow-red); border: 1px solid rgba(244, 67, 54, 0.2); } - .card-label { - font-size: 1.1em; + .card-label { + font-size: 1em; font-weight: 500; color: var(--text-secondary-color); margin-bottom: 12px; } .bonus-amount { - font-size: 3em; + font-size: 2.5em; font-weight: 800; color: var(--brand-yellow); letter-spacing: -2px; line-height: 1; } .debt-amount { - font-size: 3em; + font-size: 2.5em; font-weight: 800; color: var(--brand-red); letter-spacing: -2px; @@ -292,68 +323,71 @@ TEMPLATE = """ .client-id-label { font-weight: 500; color: var(--text-secondary-color); + font-size: 0.9em; } .client-id-value { - font-size: 1.3em; + font-size: 1.1em; font-weight: 700; color: var(--brand-yellow); - letter-spacing: 2px; + letter-spacing: 1px; background-color: rgba(255,193,7,0.1); - padding: 4px 10px; + padding: 4px 8px; border-radius: 8px; } - .navigation-buttons { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: var(--padding-m); - margin-top: var(--padding-m); + + .navigation { + display: flex; + justify-content: space-around; + gap: 10px; + margin-bottom: var(--padding-m); } + .nav-button { + flex-grow: 1; + padding: 12px; background-color: var(--card-bg); + color: var(--text-color); + border: none; border-radius: var(--border-radius); - padding: var(--padding-m); - text-align: center; + font-size: 0.9em; + font-weight: 600; 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); + transition: background-color 0.2s ease; + text-align: center; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); } .nav-button:hover { - background-color: #2a2a2c; - transform: translateY(-3px); - } - .nav-button-icon { - font-size: 1.8em; - margin-bottom: 8px; - color: var(--brand-yellow); + background-color: #3a3a3c; } - .nav-button-text { - font-size: 0.9em; - font-weight: 500; - color: var(--text-secondary-color); + .nav-button.active { + background-color: var(--brand-yellow); + color: var(--brand-black); } - .history-section { - background-color: var(--card-bg); + + .content-section { + background-color: var(--section-bg); border-radius: var(--border-radius); padding: var(--padding-l); - margin-top: var(--padding-m); + display: none; /* Hidden by default */ + } + .content-section.active { + display: block; } - .history-title { + + .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); } + + /* History Styles */ .history-list { list-style: none; padding: 0; margin: 0; - max-height: 35vh; + max-height: 40vh; /* Limit height for scrolling */ overflow-y: auto; } .history-item { @@ -364,110 +398,103 @@ TEMPLATE = """ 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-details { display: flex; flex-direction: column; flex-grow: 1; margin-right: 10px;} + .history-description { font-size: 1em; font-weight: 500; word-break: break-word; } .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 { font-size: 1.1em; font-weight: 700; flex-shrink: 0; } .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; + .history-amount.payment { color: var(--brand-green); } /* For debt payments */ + .history-amount.debt-accrual { color: var(--brand-red); } /* For adding debt */ + + /* Invoice Styles */ + .invoice-list { + list-style: none; + padding: 0; + margin: 0; + max-height: 40vh; /* Limit height for scrolling */ + overflow-y: auto; } - .modal-content { + .invoice-item { 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); + margin-bottom: var(--padding-m); + padding: var(--padding-m); + border: 1px solid rgba(255, 255, 255, 0.1); } - .modal-header { + .invoice-summary { 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); + margin-bottom: 10px; + cursor: pointer; /* Make summary clickable to toggle items */ + } + .invoice-summary .date { font-size: 0.9em; color: var(--text-secondary-color); } + .invoice-summary .total { font-size: 1.2em; font-weight: 700; color: var(--brand-yellow); } + .invoice-details { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid rgba(255, 255, 255, 0.05); + display: none; /* Hidden by default */ + } + .invoice-details.expanded { display: block; } + .invoice-item-list { list-style: none; padding: 0; margin: 0; } + .invoice-line-item { + display: flex; + justify-content: space-between; + font-size: 0.9em; + padding: 5px 0; + border-bottom: 1px dashed rgba(255, 255, 255, 0.03); } - .modal-close { - font-size: 2em; - cursor: pointer; + .invoice-line-item:last-child { border-bottom: none; } + .invoice-line-item .item-name { flex-grow: 1; margin-right: 10px; } + .invoice-line-item .item-qty-price { flex-shrink: 0; text-align: right; color: var(--text-secondary-color); } + .no-items { + text-align: center; color: var(--text-secondary-color); - line-height: 1; - } - .modal-close:hover { - color: var(--text-color); + padding: 2rem 0; } - .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); + + /* Contact Info Styles */ + .contact-info-details { + display: flex; + flex-direction: column; + gap: 15px; } - .invoice-title { - font-size: 1.3em; - font-weight: 700; - margin-bottom: var(--padding-m); - color: var(--brand-yellow); + .contact-item { display: flex; - justify-content: space-between; align-items: center; + gap: 15px; + font-size: 1em; } - .invoice-date { - font-size: 0.9em; - color: var(--text-secondary-color); - } - .invoice-item { + .contact-item a { + color: var(--text-color); + text-decoration: none; display: flex; - justify-content: space-between; - padding: 10px 0; - border-bottom: 1px dashed rgba(255, 255, 255, 0.1); - font-size: 0.95em; + align-items: center; + gap: 10px; + word-break: break-word; + } + .contact-item a:hover { + color: var(--brand-yellow); } - .invoice-item:last-child { - border-bottom: none; + .contact-item svg { + width: 24px; + height: 24px; + fill: var(--brand-yellow); + flex-shrink: 0; } - .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; + .org-name { 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; - } + margin-bottom: var(--padding-m); + } + .no-contact-info { + text-align: center; + color: var(--text-secondary-color); + padding: 2rem 0; + } + @@ -493,26 +520,17 @@ TEMPLATE = """

{{ user.id }}

- + -
-

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

+
+

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

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

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

- {% endif %} {% else %} -

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

+

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

{% endif %}
- - - +
+

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

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

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

+ {% endif %} +
+ +
+

Контакты организации

+ {% if contact_info %} +
{{ contact_info.name or 'Организация' }}
+
+ {% if contact_info.phone %} + + {% endif %} + {% if contact_info.whatsapp %} + + {% endif %} + {% if contact_info.telegram %} + + {% endif %} + {% if contact_info.address %} +
+ + {{ contact_info.address }} +
+ {% endif %} + {% if contact_info.website %} + + {% endif %} +
+ {% else %} +

Информация о компании пока не добавлена.

+ {% endif %} +
- - - - @@ -753,12 +823,14 @@ ADMIN_TEMPLATE = """ --admin-secondary: #6c757d; --admin-success: #198754; --admin-danger: #dc3545; + --admin-info: #0d6efd; --border-radius: 12px; --padding: 1.5rem; --font-family: 'Inter', sans-serif; } body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; } .container { max-width: 1200px; margin: 0 auto; } + h1, h2, h3 { color: #343a40; } h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; } .summary-bar { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--padding); margin-bottom: var(--padding); } .summary-card { background: var(--admin-card-bg); padding: var(--padding); border-radius: var(--border-radius); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); text-align: center; } @@ -766,90 +838,95 @@ ADMIN_TEMPLATE = """ .summary-card .label { font-size: 0.9em; color: var(--admin-secondary); margin-top: 0.5rem; } .summary-card .value.bonus { color: var(--admin-primary-dark); } .summary-card .value.debt { color: var(--admin-danger); } + .summary-card .value.users { color: var(--admin-info); } + .controls-bar { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; background: var(--admin-card-bg); padding: var(--padding); border-radius: var(--border-radius); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); margin-bottom: var(--padding); } - .controls-bar input[type="text"], .controls-bar input[type="tel"], .controls-bar input[type="number"] { flex-grow: 1; padding: 12px 15px; font-size: 1.1em; border-radius: 8px; border: 1px solid var(--admin-border); box-sizing: border-box; min-width: 200px; } - .btn { padding: 12px 20px; font-size: 1em; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease; } + .controls-bar input[type="text"] { flex-grow: 1; padding: 12px 15px; font-size: 1em; border-radius: 8px; border: 1px solid var(--admin-border); box-sizing: border-box; min-width: 200px; } + .btn { padding: 10px 18px; font-size: 1em; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease; } .btn-primary { background-color: var(--admin-primary); color: #000; } .btn-primary:hover { background-color: var(--admin-primary-dark); } .btn-delete { background-color: var(--admin-danger); color: white; } .btn-delete:hover { background-color: #c82333; } - .btn-edit { background-color: var(--admin-secondary); color: white; } - .btn-edit:hover { background-color: #545b62; } - .user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: var(--padding); margin-top: var(--padding); } + .btn-secondary { background-color: var(--admin-secondary); color: white; } + .btn-secondary:hover { background-color: #5a6268; } + + + .user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--padding); margin-top: var(--padding); } .user-card { background-color: var(--admin-card-bg); border-radius: var(--border-radius); padding: var(--padding); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); display: flex; flex-direction: column; transition: transform 0.2s ease, box-shadow 0.2s ease; } .user-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); } .user-info { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; } - .user-info img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 3px solid var(--admin-border); background-color: #eee; } - .user-details .name { font-weight: 600; font-size: 1.2em; } - .user-details .username { color: var(--admin-secondary); font-size: 0.9em; } + .user-info img { width: 50px; height: 50px; border-radius: 50%; object-fit: cover; border: 2px solid var(--admin-border); background-color: #eee; } + .user-details .name { font-weight: 600; font-size: 1.1em; } + .user-details .username { color: var(--admin-secondary); font-size: 0.8em; } .user-balances { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; text-align: center; margin-bottom: 1rem; } - .user-balances .label { font-size: 0.9em; color: var(--admin-secondary); } - .user-balances .amount.bonus { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); } - .user-balances .amount.debt { font-size: 1.8em; font-weight: 700; color: var(--admin-danger); } + .user-balances .label { font-size: 0.8em; color: var(--admin-secondary); } + .user-balances .amount.bonus { font-size: 1.6em; font-weight: 700; color: var(--admin-primary-dark); } + .user-balances .amount.debt { font-size: 1.6em; font-weight: 700; color: var(--admin-danger); } .user-actions { margin-top: auto; display: flex; flex-direction: column; gap: 0.5rem; } .btn-manage { display: block; width: 100%; padding: 10px; background-color: var(--admin-primary); color: #000; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s; } .btn-manage:hover { background-color: var(--admin-primary-dark); } .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; } + + /* Modals */ .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(5px); } - .modal-content { background-color: var(--admin-bg); margin: 5% auto; padding: var(--padding); border: 1px solid var(--admin-border); width: 90%; max-width: 700px; border-radius: var(--border-radius); position: relative; box-shadow: 0 8px 30px rgba(0,0,0,0.15); } + .modal-content { background-color: var(--admin-bg); margin: 3% auto; padding: var(--padding); border: 1px solid var(--admin-border); width: 95%; max-width: 800px; border-radius: var(--border-radius); position: relative; box-shadow: 0 8px 30px rgba(0,0,0,0.15); } .modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; } - .modal-close:hover { color: #777; } .modal-header { padding-bottom: 1rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--admin-border); } .modal-header h2 { margin: 0; font-size: 1.5rem; } .modal-header .username { font-size: 1rem; color: var(--admin-secondary); } + + /* Forms */ .form-section { border: 1px solid var(--admin-border); border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; } .form-section h3 { margin-top: 0; margin-bottom: 1rem; font-size: 1.1em; } - .form-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; align-items: flex-end; } + .form-section hr { margin: 1rem 0; border: 0; border-top: 1px solid rgba(0,0,0,.1); } + .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: flex-start; margin-bottom: 1rem; } + .form-row.three-cols { grid-template-columns: 2fr 1fr 1fr; gap: 0.5rem; } /* For invoice items */ .form-group { display: flex; flex-direction: column; } .form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; } - .form-group input { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; } - .calculation-summary { background: #f0f0f0; padding: 1rem; border-radius: 8px; margin-top: 1rem; } + .form-group input[type="text"], + .form-group input[type="number"], + .form-group input[type="tel"] { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; } + .form-group input[type="number"] { -moz-appearance: textfield; } /* Hide arrows in Firefox */ + .form-group input[type="number"]::-webkit-outer-spin-button, + .form-group input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } /* Hide arrows in Chrome/Safari */ + .add-item-btn { background-color: var(--admin-secondary); color: white; padding: 10px 15px; border-radius: 8px; border: none; cursor: pointer; align-self: flex-end; } + .add-item-btn:hover { background-color: #5a6268; } + .remove-item-btn { background-color: var(--admin-danger); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.8em; padding: 2px 6px;} + + .calculation-summary, .invoice-summary-preview { background: #e9ecef; padding: 1rem; border-radius: 8px; margin-top: 1rem; } .summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; } .summary-item strong { font-weight: 600; } + .summary-item.total { font-size: 1.1em; font-weight: 700; } + .invoice-item-list-preview { list-style: none; padding: 0; max-height: 150px; overflow-y: auto; margin-bottom: 1rem; } + .invoice-item-list-preview li { padding: 5px 0; border-bottom: 1px dashed rgba(0,0,0,.05); display: flex; justify-content: space-between; align-items: center;} + .invoice-item-list-preview li:last-child { border-bottom: none; } + + + /* History & Invoices */ .history-container { margin-top: 1.5rem; } .history-container h3 { font-size: 1.2rem; margin-bottom: 1rem; } - .history-list { list-style: none; padding: 0; max-height: 200px; overflow-y: auto; border: 1px solid var(--admin-border); border-radius: 8px; } + .history-list { list-style: none; padding: 0; max-height: 250px; overflow-y: auto; border: 1px solid var(--admin-border); border-radius: 8px; } .history-item { display: flex; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid var(--admin-border); } .history-item:last-child { border-bottom: none; } - .history-item .desc { font-size: 0.9em; } - .history-item .date { font-size: 0.8em; color: var(--admin-secondary); } - .history-item .amount.bonus-accrual { color: var(--admin-success); font-weight: 600; } - .history-item .amount.bonus-deduction { color: var(--admin-danger); font-weight: 600; } - .history-item .amount.debt-accrual { color: var(--admin-danger); font-weight: 600; } - .history-item .amount.debt-payment { color: var(--admin-success); font-weight: 600; } + .history-item .desc { font-size: 0.9em; flex-grow: 1; margin-right: 10px; } + .history-item .date { font-size: 0.8em; color: var(--admin-secondary); flex-shrink: 0; text-align: right; } + .history-item .amount { font-weight: 600; flex-shrink: 0; text-align: right;} + .history-item .amount.bonus-accrual, .history-item .amount.debt-payment { color: var(--admin-success); } + .history-item .amount.bonus-deduction, .history-item .amount.debt-accrual { color: var(--admin-danger); } + .history-item .amount.invoice { color: var(--admin-primary-dark); } + + /* Modal Footer */ .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;} .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; } .btn-submit { background-color: var(--admin-success); color: white; } - .btn-submit:hover { background-color: #157346; } + .btn-submit:hover { background-color: #157347; } .status-message { text-align: center; font-weight: 500; flex-grow: 1; text-align: left; } - .invoice-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--padding); margin-top: var(--padding); } - .invoice-card { background-color: var(--admin-card-bg); border-radius: var(--border-radius); padding: var(--padding); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); display: flex; flex-direction: column; transition: transform 0.2s ease, box-shadow 0.2s ease; } - .invoice-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); } - .invoice-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; border-bottom: 1px solid var(--admin-border); padding-bottom: 0.5rem; } - .invoice-header .id { font-weight: 600; font-size: 1.1em; } - .invoice-header .date { color: var(--admin-secondary); font-size: 0.9em; } - .invoice-items-list { list-style: none; padding: 0; margin: 0; border-bottom: 1px solid var(--admin-border); padding-bottom: 0.5rem; margin-bottom: 0.5rem; } - .invoice-item-admin { display: flex; justify-content: space-between; font-size: 0.9em; margin-bottom: 5px; } - .invoice-item-admin .name { flex-grow: 1; margin-right: 10px; } - .invoice-item-admin .quantity, .invoice-item-admin .price { min-width: 60px; text-align: right; } - .invoice-total-admin { text-align: right; font-weight: 600; margin-top: 0.5rem; } - .invoice-actions { margin-top: auto; display: flex; gap: 0.5rem; } - .btn-invoice-delete { background-color: var(--admin-danger); color: white; padding: 8px 15px; font-size: 0.9em; border-radius: 6px; } - .btn-invoice-delete:hover { background-color: #c82333; } - .add-item-row { display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 0.5rem; margin-bottom: 0.5rem; align-items: center; } - .add-item-row input { padding: 8px; font-size: 0.9em; border: 1px solid var(--admin-border); border-radius: 6px; } - .add-item-row .btn-add-item { padding: 8px 15px; font-size: 0.9em; border-radius: 6px; background-color: var(--admin-primary); color: #000; border: none; cursor: pointer; } - .add-item-row .btn-add-item:hover { background-color: var(--admin-primary-dark); } - .invoice-calculation-summary { background: #f0f0f0; padding: 0.75rem; border-radius: 6px; margin-top: 1rem; } - .summary-item-admin { display: flex; justify-content: space-between; margin-bottom: 0.4rem; font-size: 0.9em; } - .summary-item-admin strong { font-weight: 600; } - .no-invoices-admin { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; } - .company-info-section { background: var(--admin-card-bg); border-radius: var(--border-radius); padding: var(--padding); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); margin-bottom: var(--padding); } - .company-info-section h3 { margin-top: 0; margin-bottom: 1.5rem; font-size: 1.3em; border-bottom: 1px solid var(--admin-border); padding-bottom: 0.5rem; } - .company-info-section .form-group { margin-bottom: 1rem; } - .company-info-section .form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; } - .company-info-section .form-group input { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; } - .company-info-section .btn-save-company { display: block; width: fit-content; margin-top: 1.5rem; margin-left: auto; } + + /* Contact Info Admin */ + #contactInfoModal .form-group { margin-bottom: 1rem;} + #contactInfoModal .form-group:last-of-type { margin-bottom: 0;} + + @@ -857,7 +934,7 @@ ADMIN_TEMPLATE = """

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

-
{{ summary.total_users }}
+
{{ summary.total_users }}
Всего клиентов
@@ -869,7 +946,7 @@ ADMIN_TEMPLATE = """
Всего долгов
-
{{ summary.users_with_debt }}
+
{{ summary.users_with_debt }}
Клиенты с долгом
@@ -877,7 +954,7 @@ ADMIN_TEMPLATE = """
- +
{% if users %} @@ -887,8 +964,13 @@ ADMIN_TEMPLATE = """
User Avatar
-
{{ user.first_name or '' }} {{ user.last_name or '' }}
-
@{{ user.username if user.username else user.phone_number }} | ID: {{ user.id }}
+
{{ user.first_name or 'Без имени' }} {{ user.last_name or '' }}
+
+ {% if user.telegram_id %} @{{ user.username if user.username else 'tg user' }} {% endif %} + {% if user.phone_number %} {{ user.phone_number }} {% endif %} + {% if not user.telegram_id and not user.phone_number %} Нет контактных данных {% endif %} + | ID: {{ user.id }} +
@@ -898,12 +980,11 @@ ADMIN_TEMPLATE = """
Долг
-
{{ "%.2f"|format(user.debts|float if user.debts else 0) }}
+
{{ "%.2f"|format(user.debts|float) }}
- - + {% if user.telegram_id == None %} {% endif %} @@ -924,60 +1005,83 @@ ADMIN_TEMPLATE = """
- +
-

Бонусы

+

Операции с бонусами и долгами

- - + +
- +
-
-
Текущий баланс: 0.00
-
Будет начислено (2%): +0.00
-
Будет списано: -0.00
-
-
Итоговый баланс бонусов: 0.00
-
-
- -
-

Долги

- +
- +
+
Текущие бонусы: 0.00
+
Будет начислено (2%): +0.00
+
Будет списано бонусов: -0.00
+
Текущий долг: 0.00
-
Будет добавлено: +0.00
-
Будет погашено: -0.00
+
Будет добавлено долга: +0.00
+
Будет погашено долга: -0.00

-
Итоговый долг: 0.00
+
Итоговые бонусы: 0.00
+
Итоговый долг: 0.00
+
+
+
+

Добавить накладную

+
+ +
+
+
+
+
+
+
+
+ + +
+ +
+
+
Всего позиций: 0
+
Итоговая сумма накладной: 0.00 ₽
+
+ +
+ +
-

Общая история операций

+

Общая история операций и накладных

- + - + -