diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -19,6 +19,7 @@ app = Flask(__name__) app.secret_key = 'manolisa_secret_key_no_login' DATA_FILE = 'data.json' + SYNC_FILES = [DATA_FILE] REPO_ID = "Kgshop/manolisa" @@ -171,6 +172,7 @@ translations = { } } + def get_locale(): return session.get('lang', 'ru') @@ -187,19 +189,32 @@ def set_language(language): session['lang'] = language if language in translations else 'ru' return redirect(request.referrer or url_for('catalog')) + def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY): if not HF_TOKEN_READ and not HF_TOKEN_WRITE: logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.") + token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE + files_to_download = [specific_file] if specific_file else SYNC_FILES logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...") all_successful = True + for file_name in files_to_download: success = False for attempt in range(retries + 1): try: logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...") - local_path = hf_hub_download(repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use, local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False) + local_path = hf_hub_download( + repo_id=REPO_ID, + filename=file_name, + repo_type="dataset", + token=token_to_use, + local_dir=".", + local_dir_use_symlinks=False, + force_download=True, + resume_download=False + ) logging.info(f"Successfully downloaded {file_name} to {local_path}.") success = True break @@ -225,11 +240,14 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...") except Exception as e: logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True) + if attempt < retries: time.sleep(delay) + if not success: logging.error(f"Failed to download {file_name} after {retries + 1} attempts.") all_successful = False + logging.info(f"Download process finished. Overall success: {all_successful}") return all_successful @@ -241,10 +259,18 @@ def upload_db_to_hf(specific_file=None): api = HfApi() files_to_upload = [specific_file] if specific_file else SYNC_FILES logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...") + for file_name in files_to_upload: if os.path.exists(file_name): try: - api.upload_file(path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + api.upload_file( + path_or_fileobj=file_name, + path_in_repo=file_name, + repo_id=REPO_ID, + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) logging.info(f"File {file_name} successfully uploaded to Hugging Face.") except Exception as e: logging.error(f"Error uploading file {file_name} to Hugging Face: {e}") @@ -263,6 +289,8 @@ def periodic_backup(): upload_db_to_hf() logging.info("Periodic backup finished.") + + def load_data(): default_data = {'products': [], 'categories': [], 'seasons': [], 'orders': {}, 'employees': [], 'exchange_rate_kzt': 450.0} try: @@ -272,8 +300,12 @@ def load_data(): if not isinstance(data, dict): logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.") raise FileNotFoundError - for key, default_value in default_data.items(): - if key not in data: data[key] = default_value + if 'products' not in data: data['products'] = [] + if 'categories' not in data: data['categories'] = [] + if 'seasons' not in data: data['seasons'] = [] + if 'orders' not in data: data['orders'] = {} + if 'employees' not in data: data['employees'] = [] + if 'exchange_rate_kzt' not in data: data['exchange_rate_kzt'] = 450.0 return data except FileNotFoundError: logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.") @@ -286,8 +318,12 @@ def load_data(): if not isinstance(data, dict): logging.error(f"Downloaded {DATA_FILE} is not a dictionary. Using default.") return default_data - for key, default_value in default_data.items(): - if key not in data: data[key] = default_value + if 'products' not in data: data['products'] = [] + if 'categories' not in data: data['categories'] = [] + if 'seasons' not in data: data['seasons'] = [] + if 'orders' not in data: data['orders'] = {} + if 'employees' not in data: data['employees'] = [] + if 'exchange_rate_kzt' not in data: data['exchange_rate_kzt'] = 450.0 return data except FileNotFoundError: logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.") @@ -314,9 +350,13 @@ def save_data(data): if not isinstance(data, dict): logging.error("Attempted to save invalid data structure (not a dict). Aborting save.") return - default_data_keys = {'products': [], 'categories': [], 'seasons': [], 'orders': {}, 'employees': [], 'exchange_rate_kzt': 450.0} - for key, default_value in default_data_keys.items(): - if key not in data: data[key] = default_value + if 'products' not in data: data['products'] = [] + if 'categories' not in data: data['categories'] = [] + if 'seasons' not in data: data['seasons'] = [] + if 'orders' not in data: data['orders'] = {} + if 'employees' not in data: data['employees'] = [] + if 'exchange_rate_kzt' not in data: data['exchange_rate_kzt'] = 450.0 + with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) logging.info(f"Data successfully saved to {DATA_FILE}") @@ -324,6 +364,8 @@ def save_data(data): except Exception as e: logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True) + + CATALOG_TEMPLATE = ''' @@ -332,90 +374,95 @@ CATALOG_TEMPLATE = '''
{{ _('total_to_pay') }}: {{ order.total_price_kzt|int }} KZT
-({{ "%.2f"|format(order.total_price_usd) }} USD по курсу {{ (order.total_price_kzt / order.total_price_usd)|round(2) }})
- {% else %} -{{ _('total_to_pay') }}: {{ "%.2f"|format(order.total_price_usd) }} USD
- {% endif %} +{{ _('total_to_pay') }}: {{ "%.2f"|format(order.total_price) }} {{ order.currency }}
{{ _('order_status_desc') }}
@@ -1237,16 +1298,19 @@ ORDER_TEMPLATE = ''' const orderId = '{{ order.id }}'; const orderUrl = `{{ request.url }}`; const whatsappNumber = "+77773616116"; + let message = `{{ _('whatsapp_greeting') }}%0A%0A`; message += `*{{ _('whatsapp_order_number') }}* ${orderId}%0A`; message += `*{{ _('whatsapp_order_link') }}* ${encodeURIComponent(orderUrl)}%0A%0A`; message += `{{ _('whatsapp_contact_me') }}`; + const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${message}`; window.open(whatsappUrl, '_blank'); } + {% else %} -{{ _('order_not_found') }}
{{ _('back_to_catalog') }} {% endif %} @@ -1288,19 +1352,34 @@ ADMIN_TEMPLATE = ''' button[type="submit"] { min-width: 120px; justify-content: center; } .delete-button { background-color: #c04c4c; } .delete-button:hover { background-color: #a03c3c; } + .add-button { background-color: #008B8B; } + .add-button:hover { background-color: #20B2AA; } .item-list { display: grid; gap: 20px; } .item { background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.07); border: 1px solid #b0e0e6; } .item p { margin: 5px 0; font-size: 0.9rem; color: #5f7f7f; } .item strong { color: #2f4f4f; } .item .description { font-size: 0.85rem; color: #708090; max-height: 60px; overflow: hidden; text-overflow: ellipsis; } .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; } + .item-actions button:not(.delete-button) { background-color: #008B8B; } + .item-actions button:not(.delete-button):hover { background-color: #20B2AA; } .edit-form-container { margin-top: 15px; padding: 20px; background: #e0ffff; border: 1px dashed #008B8B; border-radius: 6px; display: none; } details { background-color: #f8ffff; border: 1px solid #b0e0e6; border-radius: 8px; margin-bottom: 20px; } - details > summary { cursor: pointer; font-weight: 600; color: #005f6b; display: block; padding: 15px; list-style: none; position: relative; } + details > summary { cursor: pointer; font-weight: 600; color: #005f6b; display: block; padding: 15px; border-bottom: 1px solid #b0e0e6; list-style: none; position: relative; } details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; color: #008B8B; } details[open] > summary::after { transform: translateY(-50%) rotate(180deg); } + details[open] > summary { border-bottom: 1px solid #b0e0e6; } + details .form-content { padding: 20px; } + .variant-input-group { display: grid; grid-template-columns: 1fr 1fr auto; align-items: center; gap: 10px; margin-bottom: 8px; padding: 10px; border: 1px solid #afeeee; border-radius: 5px; } + .variant-input-group label { margin-top: 0; } + .variant-input-group input { margin-top: 0; } + .remove-variant-btn { background-color: #c04c4c; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; } + .remove-variant-btn:hover { background-color: #a03c3c; } + .add-variant-btn { background-color: #b0e0e6; color: #2f4f4f; } + .add-variant-btn:hover { background-color: #afeeee; } .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; border: 1px solid #b0e0e6; object-fit: cover;} - .sync-buttons, .rate-form { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; } + .sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; } + .download-hf-button { background-color: #708090; } + .download-hf-button:hover { background-color: #5f7f7f; } .flex-container { display: flex; flex-wrap: wrap; gap: 20px; } .flex-item { flex: 1; min-width: 300px; } .message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-size: 0.9rem;} @@ -1311,21 +1390,19 @@ ADMIN_TEMPLATE = ''' .status-indicator.in-stock { background-color: #c6f6d5; color: #2f855a; } .status-indicator.out-of-stock { background-color: #fed7d7; color: #c53030; } .status-indicator.top-product { background-color: #feebc8; color: #9c4221; margin-left: 5px;} - .variant-block { border: 1px solid #b0e0e6; padding: 15px; border-radius: 8px; margin-bottom: 15px; background-color: #f0f8ff; } - .variant-block-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } - .variant-block-header h4 { margin: 0; color: #008B8B; }Категорий нет.
{% endfor %} -Категорий пока нет.
+ {% endif %} +Сезонов нет.
{% endfor %} + +Сезонов пока нет.
+ {% endif %}Сотрудников нет.
{% endfor %} + +Сотрудников пока нет.
+ {% endif %}
@@ -1460,59 +1612,100 @@ ADMIN_TEMPLATE = '''
Категория: {{ product.get('category', 'Без категории') }} | Сезон: {{ product.get('season', 'Без сезона') }}
-Цена: {{ "%.2f"|format(product.price) }} USD | Шт. в линейке: {{ product.get('items_per_line', 'N/A') }}
-Цвета: {{ product.get('variants', [])|map(attribute='color')|join(', ') }}
+Категория: {{ product.get('category', 'Без категории') }}
+Сезон: {{ product.get('season', 'Без сезона') }}
+Цена за линейку: {{ "%.2f"|format(product['price']) }} USD
+Шт. в линейке: {{ product.get('items_per_line', 'N/A') }}
+Описание: {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}
+ {% set colors = product.get('variants', []) | map(attribute='color') | list %} +Цвета/Вар-ты: {{ colors|join(', ') if colors else 'Нет' }} ({{ colors|length }} шт.)
Текущие фото:
-Товаров пока нет.
{% endif %}