Shveiauto commited on
Commit
96b4fd7
·
verified ·
1 Parent(s): 329a043

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +192 -79
app.py CHANGED
@@ -1,5 +1,6 @@
 
1
 
2
- from flask import Flask, render_template_string, request, redirect, url_for, session, send_file, flash
3
  import json
4
  import os
5
  import logging
@@ -10,7 +11,8 @@ from huggingface_hub import HfApi, hf_hub_download
10
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
11
  from werkzeug.utils import secure_filename
12
  from dotenv import load_dotenv
13
- import requests # Added for more specific network error handling
 
14
 
15
  load_dotenv()
16
 
@@ -31,14 +33,13 @@ CURRENCY_CODE = 'KGS'
31
  CURRENCY_NAME = 'Кыргызский сом (с)'
32
 
33
  DOWNLOAD_RETRIES = 3
34
- DOWNLOAD_DELAY = 5 # seconds
35
 
36
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
37
 
38
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
39
- if not HF_TOKEN_READ and not HF_TOKEN_WRITE: # Allow read with write token too
40
  logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
41
- # Continue attempt without token for public repos
42
 
43
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
44
 
@@ -59,22 +60,22 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
59
  local_dir=".",
60
  local_dir_use_symlinks=False,
61
  force_download=True,
62
- resume_download=False # Force fresh download each attempt
63
  )
64
  logging.info(f"Successfully downloaded {file_name} to {local_path}.")
65
  success = True
66
- break # Exit retry loop on success
67
  except RepositoryNotFoundError:
68
  logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.")
69
- return False # No point retrying if repo doesn't exist
70
  except HfHubHTTPError as e:
71
  if e.response.status_code == 404:
72
  logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Skipping this file.")
73
- success = False # Mark as failed for this specific file, but don't stop others
74
- break # No point retrying a 404
75
  else:
76
  logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
77
- except requests.exceptions.RequestException as e: # Catch network errors
78
  logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
79
  except Exception as e:
80
  logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
@@ -84,7 +85,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
84
 
85
  if not success:
86
  logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
87
- all_successful = False # Mark overall download as failed if any file fails
88
 
89
  logging.info(f"Download process finished. Overall success: {all_successful}")
90
  return all_successful
@@ -97,7 +98,7 @@ def load_data():
97
  logging.info(f"Local data loaded successfully from {DATA_FILE}")
98
  if not isinstance(data, dict):
99
  logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
100
- raise FileNotFoundError # Treat as missing to trigger download
101
  if 'products' not in data: data['products'] = []
102
  if 'categories' not in data: data['categories'] = []
103
  return data
@@ -106,10 +107,8 @@ def load_data():
106
  except json.JSONDecodeError:
107
  logging.error(f"Error decoding JSON in local {DATA_FILE}. File might be corrupt. Attempting download.")
108
 
109
- # Proceed to download only if local loading failed
110
  if download_db_from_hf(specific_file=DATA_FILE):
111
  try:
112
- # Try loading the newly downloaded file
113
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
114
  data = json.load(file)
115
  logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
@@ -130,11 +129,10 @@ def load_data():
130
  return default_data
131
  else:
132
  logging.error(f"Failed to download {DATA_FILE} from HF after retries. Using empty default data structure.")
133
- return default_data # Return empty structure in memory
134
 
135
  def save_data(data):
136
  try:
137
- # Ensure the structure is valid before saving
138
  if not isinstance(data, dict):
139
  logging.error("Attempted to save invalid data structure (not a dict). Aborting save.")
140
  return
@@ -160,10 +158,8 @@ def load_users():
160
  except json.JSONDecodeError:
161
  logging.error(f"Error decoding JSON in local {USERS_FILE}. File might be corrupt. Attempting download.")
162
 
163
- # Proceed to download only if local loading failed
164
  if download_db_from_hf(specific_file=USERS_FILE):
165
  try:
166
- # Try loading the newly downloaded file
167
  with open(USERS_FILE, 'r', encoding='utf-8') as file:
168
  users = json.load(file)
169
  logging.info(f"Users loaded successfully from {USERS_FILE} after download.")
@@ -179,7 +175,7 @@ def load_users():
179
  return default_users
180
  else:
181
  logging.error(f"Failed to download {USERS_FILE} from HF after retries. Using empty default user structure.")
182
- return default_users # Return empty structure in memory
183
 
184
  def save_users(users):
185
  try:
@@ -733,46 +729,44 @@ def catalog():
733
  alert("Корзина пуста! Добавьте товары перед заказом.");
734
  return;
735
  }
736
- let total = 0;
737
- let orderText = "🛍️ *Новый Заказ от Soola Cosmetics* 🛍️%0A";
738
- orderText += "----------------------------------------%0A";
739
- orderText += "*Детали заказа:*%0A";
740
- orderText += "----------------------------------------%0A";
741
- cart.forEach((item, index) => {
742
- const itemTotal = item.price * item.quantity;
743
- total += itemTotal;
744
- const colorText = item.color !== 'N/A' ? ` (${item.color})` : '';
745
- orderText += `${index + 1}. *${item.name}*${colorText}%0A`;
746
- orderText += ` Кол-во: ${item.quantity}%0A`;
747
- orderText += ` Цена: ${item.price.toFixed(2)} ${currencyCode}%0A`;
748
- orderText += ` *Сумма: ${itemTotal.toFixed(2)} ${currencyCode}*%0A%0A`;
749
- });
750
- orderText += "----------------------------------------%0A";
751
- orderText += `*ИТОГО: ${total.toFixed(2)} ${currencyCode}*%0A`;
752
- orderText += "----------------------------------------%0A";
753
-
754
- if (userInfo && userInfo.login) {
755
- orderText += "*Данные клиента:*%0A";
756
- orderText += `Имя: ${userInfo.first_name || ''} ${userInfo.last_name || ''}%0A`;
757
- orderText += `Логин: ${userInfo.login}%0A`;
758
- if (userInfo.phone) {
759
- orderText += `Телефон: ${userInfo.phone}%0A`;
760
- }
761
- orderText += `Страна: ${userInfo.country || 'Не указана'}%0A`;
762
- orderText += `Город: ${userInfo.city || 'Не указан'}%0A`;
763
- } else {
764
- orderText += "*Клиент не авторизован*%0A";
765
- }
766
- orderText += "----------------------------------------%0A";
767
 
768
- const now = new Date();
769
- const dateTimeString = now.toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' });
770
- orderText += `Дата заказа: ${dateTimeString}%0A`;
771
- orderText += `_Сформировано автоматически_`;
 
 
 
 
 
 
772
 
773
- const whatsappNumber = "996997703090";
774
- const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${orderText}`; // No need to encodeURIComponent here if already done piece by piece
775
- window.open(whatsappUrl, '_blank');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
776
  }
777
 
778
 
@@ -951,6 +945,137 @@ def product_detail(index):
951
  currency_code=CURRENCY_CODE
952
  )
953
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
954
  LOGIN_TEMPLATE = '''
955
  <!DOCTYPE html>
956
  <html lang="ru">
@@ -1464,7 +1589,8 @@ ADMIN_TEMPLATE = '''
1464
  if (group) {
1465
  const container = group.parentNode;
1466
  group.remove();
1467
- if (container && container.children.length === 0) {
 
1468
  const placeholderGroup = document.createElement('div');
1469
  placeholderGroup.className = 'color-input-group';
1470
  placeholderGroup.innerHTML = `
@@ -1484,10 +1610,8 @@ ADMIN_TEMPLATE = '''
1484
 
1485
  @app.route('/admin', methods=['GET', 'POST'])
1486
  def admin():
1487
- # Load data and users *before* processing POST to have the current state
1488
  data = load_data()
1489
  users = load_users()
1490
- # Ensure products and categories are lists/dicts even if load failed
1491
  products = data.get('products', [])
1492
  categories = data.get('categories', [])
1493
 
@@ -1501,7 +1625,7 @@ def admin():
1501
  if category_name and category_name not in categories:
1502
  categories.append(category_name)
1503
  categories.sort()
1504
- data['categories'] = categories # Update the main data dict
1505
  save_data(data)
1506
  logging.info(f"Category '{category_name}' added.")
1507
  flash(f"Категория '{category_name}' ус��ешно добавлена.", 'success')
@@ -1521,8 +1645,8 @@ def admin():
1521
  if product.get('category') == category_to_delete:
1522
  product['category'] = 'Без категории'
1523
  updated_count += 1
1524
- data['categories'] = categories # Update the main data dict
1525
- data['products'] = products # Update the main data dict
1526
  save_data(data)
1527
  logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
1528
  flash(f"Категория '{category_to_delete}' удалена.", 'success')
@@ -1566,7 +1690,6 @@ def admin():
1566
  if photo and photo.filename:
1567
  try:
1568
  ext = os.path.splitext(photo.filename)[1].lower()
1569
- # Basic check for common image extensions
1570
  if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
1571
  logging.warning(f"Skipping non-image file upload: {photo.filename}")
1572
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
@@ -1591,14 +1714,12 @@ def admin():
1591
  except Exception as e:
1592
  logging.error(f"Error uploading photo {photo.filename} to HF: {e}", exc_info=True)
1593
  flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
1594
- # Optionally remove temp file if it exists after error
1595
  if os.path.exists(temp_path):
1596
  try: os.remove(temp_path)
1597
  except OSError: pass
1598
  elif photo and not photo.filename:
1599
  logging.warning("Received an empty photo file object when adding product.")
1600
  try:
1601
- # Check if directory exists and is empty before removing
1602
  if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
1603
  os.rmdir(uploads_dir)
1604
  except OSError as e:
@@ -1611,7 +1732,7 @@ def admin():
1611
  'in_stock': in_stock, 'is_top': is_top
1612
  }
1613
  products.append(new_product)
1614
- data['products'] = products # Update the main data dict
1615
  save_data(data)
1616
  logging.info(f"Product '{name}' added.")
1617
  flash(f"Товар '{name}' успешно добавлен.", 'success')
@@ -1624,7 +1745,6 @@ def admin():
1624
 
1625
  try:
1626
  index = int(index_str)
1627
- # Use the 'products' list loaded at the start of the function
1628
  if not (0 <= index < len(products)):
1629
  raise IndexError("Product index out of range")
1630
  product_to_edit = products[index]
@@ -1719,9 +1839,8 @@ def admin():
1719
  elif uploaded_count == 0 and any(f.filename for f in photos_files):
1720
  flash("Не удалось загрузить новые фотографии.", "error")
1721
 
1722
- # Update the product in the main list
1723
  products[index] = product_to_edit
1724
- data['products'] = products # Update the main data dict
1725
  save_data(data)
1726
  logging.info(f"Product '{original_name}' (index {index}) updated to '{product_to_edit['name']}'.")
1727
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
@@ -1733,7 +1852,6 @@ def admin():
1733
  return redirect(url_for('admin'))
1734
  try:
1735
  index = int(index_str)
1736
- # Use the 'products' list loaded at the start of the function
1737
  if not (0 <= index < len(products)): raise IndexError("Product index out of range")
1738
  deleted_product = products.pop(index)
1739
  product_name = deleted_product.get('name', 'N/A')
@@ -1755,7 +1873,7 @@ def admin():
1755
  logging.error(f"Error deleting photos {photos_to_delete} for product '{product_name}' from HF: {e}", exc_info=True)
1756
  flash(f"Не удалось удалить фото для товара '{product_name}' с сервера.", "warning")
1757
 
1758
- data['products'] = products # Update the main data dict
1759
  save_data(data)
1760
  logging.info(f"Product '{product_name}' (original index {index}) deleted.")
1761
  flash(f"Товар '{product_name}' удален.", 'success')
@@ -1811,9 +1929,6 @@ def admin():
1811
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
1812
  return redirect(url_for('admin'))
1813
 
1814
- # GET request or after POST redirect
1815
- # Reload data and users again in case they were modified by POST and saved
1816
- # This ensures the template always shows the latest state after an action
1817
  current_data = load_data()
1818
  current_users = load_users()
1819
  display_products = current_data.get('products', [])
@@ -1854,7 +1969,6 @@ def force_download():
1854
  return redirect(url_for('admin'))
1855
 
1856
  if __name__ == '__main__':
1857
- # Initial load on startup
1858
  load_data()
1859
  load_users()
1860
 
@@ -1867,7 +1981,6 @@ if __name__ == '__main__':
1867
 
1868
  port = int(os.environ.get('PORT', 7860))
1869
  logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
1870
- # Use waitress or gunicorn in production instead of development server
1871
- # For simplicity, using Flask's dev server here
1872
  app.run(debug=False, host='0.0.0.0', port=port)
1873
 
 
 
1
+ # --- START OF FILE app.py ---
2
 
3
+ from flask import Flask, render_template_string, request, redirect, url_for, session, send_file, flash, jsonify
4
  import json
5
  import os
6
  import logging
 
11
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
12
  from werkzeug.utils import secure_filename
13
  from dotenv import load_dotenv
14
+ import requests
15
+ import base64 # Added for encoding order data
16
 
17
  load_dotenv()
18
 
 
33
  CURRENCY_NAME = 'Кыргызский сом (с)'
34
 
35
  DOWNLOAD_RETRIES = 3
36
+ DOWNLOAD_DELAY = 5
37
 
38
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
39
 
40
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
41
+ if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
42
  logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
 
43
 
44
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
45
 
 
60
  local_dir=".",
61
  local_dir_use_symlinks=False,
62
  force_download=True,
63
+ resume_download=False
64
  )
65
  logging.info(f"Successfully downloaded {file_name} to {local_path}.")
66
  success = True
67
+ break
68
  except RepositoryNotFoundError:
69
  logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.")
70
+ return False
71
  except HfHubHTTPError as e:
72
  if e.response.status_code == 404:
73
  logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Skipping this file.")
74
+ success = False
75
+ break
76
  else:
77
  logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
78
+ except requests.exceptions.RequestException as e:
79
  logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
80
  except Exception as e:
81
  logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
 
85
 
86
  if not success:
87
  logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
88
+ all_successful = False
89
 
90
  logging.info(f"Download process finished. Overall success: {all_successful}")
91
  return all_successful
 
98
  logging.info(f"Local data loaded successfully from {DATA_FILE}")
99
  if not isinstance(data, dict):
100
  logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
101
+ raise FileNotFoundError
102
  if 'products' not in data: data['products'] = []
103
  if 'categories' not in data: data['categories'] = []
104
  return data
 
107
  except json.JSONDecodeError:
108
  logging.error(f"Error decoding JSON in local {DATA_FILE}. File might be corrupt. Attempting download.")
109
 
 
110
  if download_db_from_hf(specific_file=DATA_FILE):
111
  try:
 
112
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
113
  data = json.load(file)
114
  logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
 
129
  return default_data
130
  else:
131
  logging.error(f"Failed to download {DATA_FILE} from HF after retries. Using empty default data structure.")
132
+ return default_data
133
 
134
  def save_data(data):
135
  try:
 
136
  if not isinstance(data, dict):
137
  logging.error("Attempted to save invalid data structure (not a dict). Aborting save.")
138
  return
 
158
  except json.JSONDecodeError:
159
  logging.error(f"Error decoding JSON in local {USERS_FILE}. File might be corrupt. Attempting download.")
160
 
 
161
  if download_db_from_hf(specific_file=USERS_FILE):
162
  try:
 
163
  with open(USERS_FILE, 'r', encoding='utf-8') as file:
164
  users = json.load(file)
165
  logging.info(f"Users loaded successfully from {USERS_FILE} after download.")
 
175
  return default_users
176
  else:
177
  logging.error(f"Failed to download {USERS_FILE} from HF after retries. Using empty default user structure.")
178
+ return default_users
179
 
180
  def save_users(users):
181
  try:
 
729
  alert("Корзина пуста! Добавьте товары перед заказом.");
730
  return;
731
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732
 
733
+ try {
734
+ const cartJson = JSON.stringify(cart);
735
+ const encodedCart = btoa(unescape(encodeURIComponent(cartJson))); // Base64 encode
736
+
737
+ if (encodedCart.length > 1800) { // Basic check for URL length limits
738
+ alert("Ошибка: Корзина слишком большая для создания ссылки. Пожалуйста, свяжитесь с нами напрямую.");
739
+ return;
740
+ }
741
+
742
+ const orderViewUrl = `${window.location.origin}/order_view/${encodedCart}`;
743
 
744
+ let messageText = "🛍️ *Новый Заказ Soola Cosmetics* 🛍️%0A";
745
+ messageText += "----------------------------------------%0A";
746
+ messageText += "*Пожалуйста, посмотрите детали заказа по ссылке ниже:*%0A";
747
+ messageText += encodeURIComponent(orderViewUrl) + "%0A"; // URL encode the link itself for safety
748
+ messageText += "----------------------------------------%0A";
749
+
750
+ if (userInfo && userInfo.login) {
751
+ messageText += "*Клиент:*%0A";
752
+ messageText += `${userInfo.first_name || ''} ${userInfo.last_name || ''} (${userInfo.login})%0A`;
753
+ } else {
754
+ messageText += "*Клиент не авторизован*%0A";
755
+ }
756
+ messageText += "----------------------------------------%0A";
757
+ const now = new Date();
758
+ const dateTimeString = now.toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' });
759
+ messageText += `Дата запроса: ${dateTimeString}`;
760
+
761
+ const whatsappNumber = "996997703090";
762
+ const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${messageText}`;
763
+
764
+ window.open(whatsappUrl, '_blank');
765
+
766
+ } catch (e) {
767
+ console.error("Error creating WhatsApp order link:", e);
768
+ alert("Произошла ошибка при формировании ссылки для заказа. Пожалуйста, попробуйте еще раз или свяжитесь с нами.");
769
+ }
770
  }
771
 
772
 
 
945
  currency_code=CURRENCY_CODE
946
  )
947
 
948
+ ORDER_VIEW_TEMPLATE = '''
949
+ <!DOCTYPE html>
950
+ <html lang="ru">
951
+ <head>
952
+ <meta charset="UTF-8">
953
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
954
+ <title>Детали Заказа - Soola Cosmetics</title>
955
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
956
+ <style>
957
+ body { font-family: 'Poppins', sans-serif; background: #f0f9f4; color: #2d332f; line-height: 1.6; padding: 20px; }
958
+ .container { max-width: 800px; margin: 20px auto; background: #fff; padding: 30px; border-radius: 10px; box-shadow: 0 3px 15px rgba(0,0,0,0.1); }
959
+ h1 { color: #1C6758; text-align: center; margin-bottom: 25px; font-size: 1.8rem; font-weight: 600; }
960
+ h2 { color: #164B41; margin-top: 30px; margin-bottom: 15px; font-size: 1.3rem; border-bottom: 1px solid #d1e7dd; padding-bottom: 8px; }
961
+ .order-item { display: flex; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid #e1f0e9; }
962
+ .order-item:last-child { border-bottom: none; }
963
+ .order-item img { width: 70px; height: 70px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; border: 1px solid #e1f0e9; flex-shrink: 0;}
964
+ .item-details { flex-grow: 1; }
965
+ .item-details strong { display: block; font-size: 1.1rem; color: #2d332f; margin-bottom: 5px; }
966
+ .item-meta { font-size: 0.9rem; color: #5e6e68; }
967
+ .item-total { font-weight: bold; text-align: right; font-size: 1rem; color: #1C6758; }
968
+ .summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid #1C6758; text-align: right; }
969
+ .summary p { margin: 8px 0; font-size: 1.1rem; }
970
+ .summary strong { font-size: 1.3rem; color: #1C6758; }
971
+ .customer-info { margin-top: 20px; background-color: #f8fcfb; padding: 15px; border-radius: 8px; border: 1px solid #d1e7dd; font-size: 0.95rem; }
972
+ .customer-info p { margin: 4px 0; }
973
+ .footer-note { text-align: center; margin-top: 30px; font-size: 0.85rem; color: #7a8d85; }
974
+ .error-message { color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 8px; text-align: center; font-weight: 500; }
975
+ </style>
976
+ </head>
977
+ <body>
978
+ <div class="container">
979
+ <h1>Детали Заказа</h1>
980
+
981
+ {% if error %}
982
+ <p class="error-message">{{ error }}</p>
983
+ {% else %}
984
+ {% if cart_items %}
985
+ <h2>Товары в заказе</h2>
986
+ {% for item in cart_items %}
987
+ <div class="order-item">
988
+ <img src="{{ item.photo_url }}" alt="{{ item.name }}">
989
+ <div class="item-details">
990
+ <strong>{{ item.name }}{{ item.color_text }}</strong>
991
+ <p class="item-meta">Цена: {{ "%.2f"|format(item.price) }} {{ currency_code }}</p>
992
+ <p class="item-meta">Количество: {{ item.quantity }}</p>
993
+ </div>
994
+ <div class="item-total">{{ "%.2f"|format(item.item_total) }} {{ currency_code }}</div>
995
+ </div>
996
+ {% endfor %}
997
+
998
+ <div class="summary">
999
+ <p><strong>Общая сумма заказа: {{ "%.2f"|format(total_price) }} {{ currency_code }}</strong></p>
1000
+ </div>
1001
+
1002
+ {% if user_info %}
1003
+ <div class="customer-info">
1004
+ <h2>Информация о клиенте</h2>
1005
+ <p><strong>Имя:</strong> {{ user_info.get('first_name', '') }} {{ user_info.get('last_name', '') }}</p>
1006
+ <p><strong>Логин:</strong> {{ user_info.get('login', 'N/A') }}</p>
1007
+ <p><strong>Телефон:</strong> {{ user_info.get('phone', 'Не указан') }}</p>
1008
+ <p><strong>Страна:</strong> {{ user_info.get('country', 'Не указана') }}</p>
1009
+ <p><strong>Город:</strong> {{ user_info.get('city', 'Не указан') }}</p>
1010
+ </div>
1011
+ {% endif %}
1012
+
1013
+ {% else %}
1014
+ <p style="text-align: center; padding: 30px;">Не удалось загрузить детали заказа или корзина пуста.</p>
1015
+ {% endif %}
1016
+ <p class="footer-note">Заказ сформирован: {{ generation_time }}</p>
1017
+ {% endif %}
1018
+ </div>
1019
+ </body>
1020
+ </html>
1021
+ '''
1022
+
1023
+ @app.route('/order_view/<encoded_cart>')
1024
+ def order_view(encoded_cart):
1025
+ cart_items_processed = []
1026
+ total_price = 0
1027
+ error_message = None
1028
+ user_info_data = session.get('user_info') # Get user info from session
1029
+
1030
+ try:
1031
+ decoded_bytes = base64.urlsafe_b64decode(encoded_cart + '=' * (-len(encoded_cart) % 4))
1032
+ cart_json = decoded_bytes.decode('utf-8')
1033
+ cart_items = json.loads(cart_json)
1034
+
1035
+ if not isinstance(cart_items, list):
1036
+ raise ValueError("Decoded data is not a list")
1037
+
1038
+ for item in cart_items:
1039
+ if not isinstance(item, dict) or 'name' not in item or 'price' not in item or 'quantity' not in item:
1040
+ logging.warning(f"Skipping invalid item in decoded cart: {item}")
1041
+ continue
1042
+
1043
+ item_total = float(item['price']) * int(item['quantity'])
1044
+ total_price += item_total
1045
+ photo_url = (f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}"
1046
+ if item.get('photo') else "https://via.placeholder.com/70x70.png?text=N/A")
1047
+ color_text = f" (Цвет: {item['color']})" if item.get('color') and item['color'] != 'N/A' else ''
1048
+
1049
+ cart_items_processed.append({
1050
+ 'name': item['name'],
1051
+ 'price': float(item['price']),
1052
+ 'quantity': int(item['quantity']),
1053
+ 'photo_url': photo_url,
1054
+ 'color_text': color_text,
1055
+ 'item_total': item_total
1056
+ })
1057
+
1058
+ except (base64.binascii.Error, UnicodeDecodeError, json.JSONDecodeError, ValueError, TypeError) as e:
1059
+ logging.error(f"Error decoding or processing order data from URL: {e}", exc_info=True)
1060
+ error_message = "Не удалось расшифровать или обработать данные заказа. Ссылка может быть повреждена или неверна."
1061
+ except Exception as e:
1062
+ logging.error(f"Unexpected error processing order view: {e}", exc_info=True)
1063
+ error_message = "Произошла непредвиденная ошибка при отображении заказа."
1064
+
1065
+ generation_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1066
+
1067
+ return render_template_string(
1068
+ ORDER_VIEW_TEMPLATE,
1069
+ cart_items=cart_items_processed,
1070
+ total_price=total_price,
1071
+ currency_code=CURRENCY_CODE,
1072
+ repo_id=REPO_ID,
1073
+ error=error_message,
1074
+ user_info=user_info_data,
1075
+ generation_time=generation_time
1076
+ )
1077
+
1078
+
1079
  LOGIN_TEMPLATE = '''
1080
  <!DOCTYPE html>
1081
  <html lang="ru">
 
1589
  if (group) {
1590
  const container = group.parentNode;
1591
  group.remove();
1592
+ // Add a new empty input if the last one was removed, except for the add form
1593
+ if (container && container.children.length === 0 && !container.id.startsWith('add-color-inputs')) {
1594
  const placeholderGroup = document.createElement('div');
1595
  placeholderGroup.className = 'color-input-group';
1596
  placeholderGroup.innerHTML = `
 
1610
 
1611
  @app.route('/admin', methods=['GET', 'POST'])
1612
  def admin():
 
1613
  data = load_data()
1614
  users = load_users()
 
1615
  products = data.get('products', [])
1616
  categories = data.get('categories', [])
1617
 
 
1625
  if category_name and category_name not in categories:
1626
  categories.append(category_name)
1627
  categories.sort()
1628
+ data['categories'] = categories
1629
  save_data(data)
1630
  logging.info(f"Category '{category_name}' added.")
1631
  flash(f"Категория '{category_name}' ус��ешно добавлена.", 'success')
 
1645
  if product.get('category') == category_to_delete:
1646
  product['category'] = 'Без категории'
1647
  updated_count += 1
1648
+ data['categories'] = categories
1649
+ data['products'] = products
1650
  save_data(data)
1651
  logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
1652
  flash(f"Категория '{category_to_delete}' удалена.", 'success')
 
1690
  if photo and photo.filename:
1691
  try:
1692
  ext = os.path.splitext(photo.filename)[1].lower()
 
1693
  if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
1694
  logging.warning(f"Skipping non-image file upload: {photo.filename}")
1695
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
 
1714
  except Exception as e:
1715
  logging.error(f"Error uploading photo {photo.filename} to HF: {e}", exc_info=True)
1716
  flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
 
1717
  if os.path.exists(temp_path):
1718
  try: os.remove(temp_path)
1719
  except OSError: pass
1720
  elif photo and not photo.filename:
1721
  logging.warning("Received an empty photo file object when adding product.")
1722
  try:
 
1723
  if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
1724
  os.rmdir(uploads_dir)
1725
  except OSError as e:
 
1732
  'in_stock': in_stock, 'is_top': is_top
1733
  }
1734
  products.append(new_product)
1735
+ data['products'] = products
1736
  save_data(data)
1737
  logging.info(f"Product '{name}' added.")
1738
  flash(f"Товар '{name}' успешно добавлен.", 'success')
 
1745
 
1746
  try:
1747
  index = int(index_str)
 
1748
  if not (0 <= index < len(products)):
1749
  raise IndexError("Product index out of range")
1750
  product_to_edit = products[index]
 
1839
  elif uploaded_count == 0 and any(f.filename for f in photos_files):
1840
  flash("Не удалось загрузить новые фотографии.", "error")
1841
 
 
1842
  products[index] = product_to_edit
1843
+ data['products'] = products
1844
  save_data(data)
1845
  logging.info(f"Product '{original_name}' (index {index}) updated to '{product_to_edit['name']}'.")
1846
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
 
1852
  return redirect(url_for('admin'))
1853
  try:
1854
  index = int(index_str)
 
1855
  if not (0 <= index < len(products)): raise IndexError("Product index out of range")
1856
  deleted_product = products.pop(index)
1857
  product_name = deleted_product.get('name', 'N/A')
 
1873
  logging.error(f"Error deleting photos {photos_to_delete} for product '{product_name}' from HF: {e}", exc_info=True)
1874
  flash(f"Не удалось удалить фото для товара '{product_name}' с сервера.", "warning")
1875
 
1876
+ data['products'] = products
1877
  save_data(data)
1878
  logging.info(f"Product '{product_name}' (original index {index}) deleted.")
1879
  flash(f"Товар '{product_name}' удален.", 'success')
 
1929
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
1930
  return redirect(url_for('admin'))
1931
 
 
 
 
1932
  current_data = load_data()
1933
  current_users = load_users()
1934
  display_products = current_data.get('products', [])
 
1969
  return redirect(url_for('admin'))
1970
 
1971
  if __name__ == '__main__':
 
1972
  load_data()
1973
  load_users()
1974
 
 
1981
 
1982
  port = int(os.environ.get('PORT', 7860))
1983
  logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
 
 
1984
  app.run(debug=False, host='0.0.0.0', port=port)
1985
 
1986
+ # --- END OF FILE app.py ---