Shveiauto commited on
Commit
afe21a4
·
verified ·
1 Parent(s): 52bc3f7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +64 -488
app.py CHANGED
@@ -1,6 +1,7 @@
 
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,19 +12,19 @@ from huggingface_hub import HfApi, hf_hub_download
11
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
12
  from werkzeug.utils import secure_filename
13
  from dotenv import load_dotenv
14
- import requests # Added for more specific network error handling
15
- import uuid # For unique order IDs
16
 
17
  load_dotenv()
18
 
19
  app = Flask(__name__)
20
- app.secret_key = 'your_unique_secret_key_soola_cosmetics_67890'
21
- DATA_FILE = 'data_soola.json'
22
- USERS_FILE = 'users_soola.json'
23
 
24
- SYNC_FILES = [DATA_FILE, USERS_FILE]
25
 
26
- REPO_ID = "Kgshop/medimaturkey" # Replace with your actual repo ID if different
27
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
28
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
29
 
@@ -40,9 +41,8 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
40
  # --- Hugging Face Sync Functions ---
41
 
42
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
43
- if not HF_TOKEN_READ and not HF_TOKEN_WRITE: # Allow read with write token too
44
  logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
45
- # Continue attempt without token for public repos
46
 
47
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
48
 
@@ -63,35 +63,30 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
63
  local_dir=".",
64
  local_dir_use_symlinks=False,
65
  force_download=True,
66
- resume_download=False # Force fresh download each attempt
67
  )
68
  logging.info(f"Successfully downloaded {file_name} to {local_path}.")
69
  success = True
70
- break # Exit retry loop on success
71
  except RepositoryNotFoundError:
72
  logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.")
73
- return False # No point retrying if repo doesn't exist
74
  except HfHubHTTPError as e:
75
  if e.response.status_code == 404:
76
  logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Skipping this file.")
77
- # Check if it's the first attempt; if so, create an empty default file locally
78
  if attempt == 0 and not os.path.exists(file_name):
79
  try:
80
  if file_name == DATA_FILE:
81
  with open(file_name, 'w', encoding='utf-8') as f:
82
  json.dump({'products': [], 'categories': [], 'orders': {}}, f)
83
  logging.info(f"Created empty local file {file_name} because it was not found on HF.")
84
- elif file_name == USERS_FILE:
85
- with open(file_name, 'w', encoding='utf-8') as f:
86
- json.dump({}, f)
87
- logging.info(f"Created empty local file {file_name} because it was not found on HF.")
88
  except Exception as create_e:
89
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
90
- success = False # Mark as failed for this specific file, but don't stop others
91
- break # No point retrying a 404
92
  else:
93
  logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
94
- except requests.exceptions.RequestException as e: # Catch network errors
95
  logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
96
  except Exception as e:
97
  logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
@@ -101,7 +96,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
101
 
102
  if not success:
103
  logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
104
- all_successful = False # Mark overall download as failed if any file fails
105
 
106
  logging.info(f"Download process finished. Overall success: {all_successful}")
107
  return all_successful
@@ -154,20 +149,18 @@ def load_data():
154
  logging.info(f"Local data loaded successfully from {DATA_FILE}")
155
  if not isinstance(data, dict):
156
  logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
157
- raise FileNotFoundError # Treat as missing to trigger download
158
  if 'products' not in data: data['products'] = []
159
  if 'categories' not in data: data['categories'] = []
160
- if 'orders' not in data: data['orders'] = {} # Initialize orders if missing
161
  return data
162
  except FileNotFoundError:
163
  logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
164
  except json.JSONDecodeError:
165
  logging.error(f"Error decoding JSON in local {DATA_FILE}. File might be corrupt. Attempting download.")
166
 
167
- # Proceed to download only if local loading failed
168
  if download_db_from_hf(specific_file=DATA_FILE):
169
  try:
170
- # Try loading the newly downloaded file
171
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
172
  data = json.load(file)
173
  logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
@@ -176,7 +169,7 @@ def load_data():
176
  return default_data
177
  if 'products' not in data: data['products'] = []
178
  if 'categories' not in data: data['categories'] = []
179
- if 'orders' not in data: data['orders'] = {} # Initialize orders if missing
180
  return data
181
  except FileNotFoundError:
182
  logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.")
@@ -189,7 +182,6 @@ def load_data():
189
  return default_data
190
  else:
191
  logging.error(f"Failed to download {DATA_FILE} from HF after retries. Using empty default data structure.")
192
- # Create empty file locally if download failed and file doesn't exist
193
  if not os.path.exists(DATA_FILE):
194
  try:
195
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
@@ -197,17 +189,16 @@ def load_data():
197
  logging.info(f"Created empty local file {DATA_FILE} after failed download.")
198
  except Exception as create_e:
199
  logging.error(f"Failed to create empty local file {DATA_FILE}: {create_e}")
200
- return default_data # Return empty structure in memory
201
 
202
  def save_data(data):
203
  try:
204
- # Ensure the structure is valid before saving
205
  if not isinstance(data, dict):
206
  logging.error("Attempted to save invalid data structure (not a dict). Aborting save.")
207
  return
208
  if 'products' not in data: data['products'] = []
209
  if 'categories' not in data: data['categories'] = []
210
- if 'orders' not in data: data['orders'] = {} # Ensure orders key exists
211
 
212
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
213
  json.dump(data, file, ensure_ascii=False, indent=4)
@@ -216,59 +207,6 @@ def save_data(data):
216
  except Exception as e:
217
  logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
218
 
219
- def load_users():
220
- default_users = {}
221
- try:
222
- with open(USERS_FILE, 'r', encoding='utf-8') as file:
223
- users = json.load(file)
224
- logging.info(f"Local users loaded successfully from {USERS_FILE}")
225
- return users if isinstance(users, dict) else default_users
226
- except FileNotFoundError:
227
- logging.warning(f"Local file {USERS_FILE} not found. Attempting download from HF.")
228
- except json.JSONDecodeError:
229
- logging.error(f"Error decoding JSON in local {USERS_FILE}. File might be corrupt. Attempting download.")
230
-
231
- # Proceed to download only if local loading failed
232
- if download_db_from_hf(specific_file=USERS_FILE):
233
- try:
234
- # Try loading the newly downloaded file
235
- with open(USERS_FILE, 'r', encoding='utf-8') as file:
236
- users = json.load(file)
237
- logging.info(f"Users loaded successfully from {USERS_FILE} after download.")
238
- return users if isinstance(users, dict) else default_users
239
- except FileNotFoundError:
240
- logging.error(f"File {USERS_FILE} still not found after download reported success. Using default.")
241
- return default_users
242
- except json.JSONDecodeError:
243
- logging.error(f"Error decoding JSON in downloaded {USERS_FILE}. Using default.")
244
- return default_users
245
- except Exception as e:
246
- logging.error(f"Unknown error loading downloaded {USERS_FILE}: {e}. Using default.", exc_info=True)
247
- return default_users
248
- else:
249
- logging.error(f"Failed to download {USERS_FILE} from HF after retries. Using empty default user structure.")
250
- # Create empty file locally if download failed and file doesn't exist
251
- if not os.path.exists(USERS_FILE):
252
- try:
253
- with open(USERS_FILE, 'w', encoding='utf-8') as f:
254
- json.dump(default_users, f)
255
- logging.info(f"Created empty local file {USERS_FILE} after failed download.")
256
- except Exception as create_e:
257
- logging.error(f"Failed to create empty local file {USERS_FILE}: {create_e}")
258
- return default_users # Return empty structure in memory
259
-
260
- def save_users(users):
261
- try:
262
- if not isinstance(users, dict):
263
- logging.error("Attempted to save invalid users structure (not a dict). Aborting save.")
264
- return
265
- with open(USERS_FILE, 'w', encoding='utf-8') as file:
266
- json.dump(users, file, ensure_ascii=False, indent=4)
267
- logging.info(f"User data successfully saved to {USERS_FILE}")
268
- upload_db_to_hf(specific_file=USERS_FILE)
269
- except Exception as e:
270
- logging.error(f"Error saving user data to {USERS_FILE}: {e}", exc_info=True)
271
-
272
  # --- Templates ---
273
 
274
  CATALOG_TEMPLATE = '''
@@ -289,12 +227,6 @@ CATALOG_TEMPLATE = '''
289
  .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #d1e7dd; }
290
  body.dark-mode .header { border-bottom-color: #2c4a41; }
291
  .header h1 { font-size: 1.8rem; font-weight: 600; color: #1C6758; }
292
- .auth-links { display: flex; gap: 15px; align-items: center; }
293
- .auth-links a { color: #3D8361; text-decoration: none; font-weight: 500; }
294
- .auth-links a:hover { text-decoration: underline; }
295
- body.dark-mode .auth-links a { color: #55a683; }
296
- .auth-links span { font-weight: 500; }
297
- body.dark-mode .auth-links span { color: #b0c8c1;}
298
  .theme-toggle { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #7a8d85; transition: color 0.3s ease; }
299
  .theme-toggle:hover { color: #3D8361; }
300
  body.dark-mode .theme-toggle { color: #8aa39a; }
@@ -321,7 +253,6 @@ CATALOG_TEMPLATE = '''
321
  body.dark-mode .product:hover { box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); }
322
  .product-image { width: 100%; aspect-ratio: 1 / 1; background-color: #fff; border-radius: 10px 10px 0 0; overflow: hidden; display: flex; justify-content: center; align-items: center; margin-bottom: 0; }
323
  .product-image img { max-width: 100%; max-height: 100%; object-fit: contain; transition: transform 0.3s ease; }
324
- /* Removed hover effect from image itself to avoid double scaling */
325
  .product-info { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; justify-content: center; }
326
  .product h2 { font-size: 1.1rem; font-weight: 600; margin: 0 0 8px 0; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #2d332f; }
327
  body.dark-mode .product h2 { color: #c8d8d3; }
@@ -382,14 +313,6 @@ CATALOG_TEMPLATE = '''
382
  <div class="container">
383
  <div class="header">
384
  <h1>MedinaTurkey.kz</h1>
385
- <div class="auth-links">
386
- {% if is_authenticated %}
387
- <span>Привет, {{ session.get('user_info', {}).get('first_name', session['user']) }}!</span>
388
- <a href="{{ url_for('logout') }}">Выйти</a>
389
- {% else %}
390
- <a href="{{ url_for('login') }}">Войти</a>
391
- {% endif %}
392
- </div>
393
  <button class="theme-toggle" onclick="toggleTheme()" aria-label="Переключить тему">
394
  <i class="fas fa-moon"></i>
395
  </button>
@@ -428,20 +351,14 @@ CATALOG_TEMPLATE = '''
428
  </div>
429
  <div class="product-info">
430
  <h2>{{ product['name'] }}</h2>
431
- {% if is_authenticated %}
432
  <div class="product-price">{{ "%.2f"|format(product['price']) }} {{ currency_code }}</div>
433
- {% else %}
434
- <div class="product-price">Цена доступна после входа</div>
435
- {% endif %}
436
  <p class="product-description">{{ product.get('description', '')[:50] }}{% if product.get('description', '')|length > 50 %}...{% endif %}</p>
437
  </div>
438
  <div class="product-actions">
439
  <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
440
- {% if is_authenticated %}
441
  <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">
442
  <i class="fas fa-cart-plus"></i> В корзину
443
  </button>
444
- {% endif %}
445
  </div>
446
  </div>
447
  {% endfor %}
@@ -501,8 +418,6 @@ CATALOG_TEMPLATE = '''
501
  const products = {{ products|tojson }};
502
  const repoId = '{{ repo_id }}';
503
  const currencyCode = '{{ currency_code }}';
504
- const isAuthenticated = {{ is_authenticated|tojson }};
505
- const userInfo = {{ session.get('user_info', {})|tojson }};
506
  let selectedProductIndex = null;
507
  let cart = JSON.parse(localStorage.getItem('soolaCart') || '[]');
508
 
@@ -524,31 +439,6 @@ CATALOG_TEMPLATE = '''
524
  }
525
  }
526
 
527
- function attemptAutoLogin() {
528
- const storedUser = localStorage.getItem('soolaUser');
529
- if (storedUser && !isAuthenticated) {
530
- console.log('Attempting auto-login for:', storedUser);
531
- fetch('/auto_login', {
532
- method: 'POST',
533
- headers: { 'Content-Type': 'application/json' },
534
- body: JSON.stringify({ login: storedUser })
535
- })
536
- .then(response => {
537
- if (response.ok) {
538
- console.log('Auto-login successful, reloading...');
539
- window.location.reload();
540
- } else {
541
- response.text().then(text => console.log(`Auto-login failed: ${response.status} ${text}`));
542
- localStorage.removeItem('soolaUser');
543
- }
544
- })
545
- .catch(error => {
546
- console.error('Auto-login fetch error:', error);
547
- localStorage.removeItem('soolaUser');
548
- });
549
- }
550
- }
551
-
552
  function openModal(index) {
553
  loadProductDetails(index);
554
  const modal = document.getElementById('productModal');
@@ -605,11 +495,6 @@ CATALOG_TEMPLATE = '''
605
  }
606
 
607
  function openQuantityModal(index) {
608
- if (!isAuthenticated) {
609
- alert('Пожалуйста, войдите в систему, чтобы добавить товар в корзину.');
610
- window.location.href = '{{ url_for("login") }}';
611
- return;
612
- }
613
  selectedProductIndex = index;
614
  const product = products[index];
615
  if (!product) {
@@ -673,7 +558,7 @@ CATALOG_TEMPLATE = '''
673
  cart[existingItemIndex].quantity += quantity;
674
  } else {
675
  cart.push({
676
- id: cartItemId, // Unique ID for the item *variant* in the cart
677
  name: product.name,
678
  price: product.price,
679
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
@@ -748,7 +633,7 @@ CATALOG_TEMPLATE = '''
748
  function removeFromCart(itemId) {
749
  cart = cart.filter(item => item.id !== itemId);
750
  localStorage.setItem('soolaCart', JSON.stringify(cart));
751
- openCartModal(); // Refresh cart modal view
752
  updateCartButton();
753
  }
754
 
@@ -756,7 +641,7 @@ CATALOG_TEMPLATE = '''
756
  if (confirm("Вы уверены, что хотите очистить корзину?")) {
757
  cart = [];
758
  localStorage.removeItem('soolaCart');
759
- openCartModal(); // Refresh cart modal view
760
  updateCartButton();
761
  }
762
  }
@@ -768,11 +653,9 @@ CATALOG_TEMPLATE = '''
768
  }
769
 
770
  const orderData = {
771
- cart: cart,
772
- userInfo: isAuthenticated ? userInfo : null // Include user info if logged in
773
  };
774
 
775
- // Disable button to prevent multiple clicks
776
  const formulateButton = document.querySelector('.formulate-order-button');
777
  if (formulateButton) formulateButton.disabled = true;
778
 
@@ -791,11 +674,11 @@ CATALOG_TEMPLATE = '''
791
  })
792
  .then(data => {
793
  if (data.order_id) {
794
- localStorage.removeItem('soolaCart'); // Clear cart on success
795
- cart = []; // Clear local cart variable
796
- updateCartButton(); // Update button display
797
- closeModal('cartModal'); // Close cart modal
798
- window.location.href = `/order/${data.order_id}`; // Redirect to order page
799
  } else {
800
  throw new Error('Не получен ID заказа от сервера.');
801
  }
@@ -803,7 +686,7 @@ CATALOG_TEMPLATE = '''
803
  .catch(error => {
804
  console.error('Ошибка при формировании заказа:', error);
805
  alert(`Ошибка: ${error.message}`);
806
- if (formulateButton) formulateButton.disabled = false; // Re-enable button on error
807
  });
808
  }
809
 
@@ -860,17 +743,16 @@ CATALOG_TEMPLATE = '''
860
  filterProducts();
861
  });
862
  });
863
- filterProducts(); // Initial filter application
864
  }
865
 
866
  function showNotification(message, duration = 3000) {
867
  const placeholder = document.getElementById('notification-placeholder');
868
  if (!placeholder) {
869
- // Create placeholder if it doesn't exist
870
  const newPlaceholder = document.createElement('div');
871
  newPlaceholder.id = 'notification-placeholder';
872
  newPlaceholder.style.position = 'fixed';
873
- newPlaceholder.style.bottom = '80px'; // Adjust position as needed
874
  newPlaceholder.style.left = '50%';
875
  newPlaceholder.style.transform = 'translateX(-50%)';
876
  newPlaceholder.style.zIndex = '1002';
@@ -884,32 +766,27 @@ CATALOG_TEMPLATE = '''
884
  notification.textContent = message;
885
  placeholder.appendChild(notification);
886
 
887
- // Force reflow before adding class for transition
888
  void notification.offsetWidth;
889
 
890
  notification.classList.add('show');
891
 
892
  setTimeout(() => {
893
  notification.classList.remove('show');
894
- // Remove element after transition finishes
895
  notification.addEventListener('transitionend', () => notification.remove());
896
  }, duration);
897
  }
898
 
899
  document.addEventListener('DOMContentLoaded', () => {
900
  applyInitialTheme();
901
- attemptAutoLogin();
902
  updateCartButton();
903
  setupFilters();
904
 
905
- // Close modal on background click
906
  window.addEventListener('click', function(event) {
907
  if (event.target.classList.contains('modal')) {
908
  closeModal(event.target.id);
909
  }
910
  });
911
 
912
- // Close modal on Escape key press
913
  window.addEventListener('keydown', function(event) {
914
  if (event.key === 'Escape') {
915
  document.querySelectorAll('.modal[style*="display: block"]').forEach(modal => {
@@ -954,11 +831,7 @@ PRODUCT_DETAIL_TEMPLATE = '''
954
 
955
  <div style="margin-top: 20px; font-size: 1rem; line-height: 1.7;">
956
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
957
- {% if is_authenticated %}
958
- <p style="font-size: 1.2rem; font-weight: bold; color: #1C6758;"><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
959
- {% else %}
960
- <p><strong>Цена:</strong> <a href="{{ url_for('login') }}" style="color: #3D8361; text-decoration: underline;">Доступна после входа</a></p>
961
- {% endif %}
962
  <p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\\n', '<br>')|safe }}</p>
963
  {% set colors = product.get('colors', []) %}
964
  {% if colors and colors|select('ne', '')|list|length > 0 %}
@@ -968,67 +841,6 @@ PRODUCT_DETAIL_TEMPLATE = '''
968
  </div>
969
  '''
970
 
971
- LOGIN_TEMPLATE = '''
972
- <!DOCTYPE html>
973
- <html lang="ru">
974
- <head>
975
- <meta charset="UTF-8">
976
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
977
- <title>Вход - Soola Cosmetics</title>
978
- <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
979
- <style>
980
- body { font-family: 'Poppins', sans-serif; background: linear-gradient(135deg, #d1e7dd, #e9f5f0); display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 20px; }
981
- .container { max-width: 400px; width: 100%; background: #fff; padding: 30px 40px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.1); text-align: center; }
982
- h2 { color: #1C6758; margin-bottom: 25px; font-weight: 600; }
983
- label { display: block; text-align: left; margin: 15px 0 5px; font-weight: 500; color: #44524c; }
984
- input[type="text"], input[type="password"] { width: 100%; padding: 12px; margin-bottom: 15px; border: 1px solid #c4d9d1; border-radius: 8px; box-sizing: border-box; font-size: 1rem; }
985
- input:focus { border-color: #1C6758; outline: none; box-shadow: 0 0 0 2px rgba(28, 103, 88, 0.2); }
986
- button { width: 100%; padding: 12px; background-color: #1C6758; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 1rem; font-weight: 600; transition: background-color 0.3s ease; margin-top: 10px; }
987
- button:hover { background-color: #164B41; }
988
- .error { color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 8px; margin-bottom: 15px; font-size: 0.9rem; text-align: left;}
989
- .back-link { display: inline-block; margin-top: 20px; color: #3D8361; text-decoration: none; font-size: 0.9rem; }
990
- .back-link:hover { text-decoration: underline; }
991
- </style>
992
- </head>
993
- <body>
994
- <div class="container">
995
- <h2>Вход в Soola Cosmetics</h2>
996
- {% if error %}
997
- <p class="error">{{ error }}</p>
998
- {% endif %}
999
- <form method="POST">
1000
- <label for="login">Логин:</label>
1001
- <input type="text" id="login" name="login" required>
1002
- <label for="password">Пароль:</label>
1003
- <input type="password" id="password" name="password" required>
1004
- <button type="submit">Войти</button>
1005
- </form>
1006
- <a href="{{ url_for('catalog') }}" class="back-link">← Вернуться в каталог</a>
1007
- </div>
1008
- </body>
1009
- </html>
1010
- '''
1011
-
1012
- LOGOUT_REDIRECT_TEMPLATE = '''
1013
- <!DOCTYPE html><html><head><title>Выход...</title></head><body>
1014
- <script>
1015
- try { localStorage.removeItem('soolaUser'); } catch (e) { console.error("Error removing from localStorage:", e); }
1016
- window.location.href = "/";
1017
- </script>
1018
- <p>Выход выполнен. Перенаправление на <a href="/">главную страницу</a>...</p>
1019
- </body></html>
1020
- '''
1021
-
1022
- LOGIN_REDIRECT_TEMPLATE = '''
1023
- <!DOCTYPE html><html><head><title>Перенаправление...</title></head><body>
1024
- <script>
1025
- try {{ localStorage.setItem('soolaUser', '{login}'); }} catch (e) {{ console.error("Error saving to localStorage:", e); }}
1026
- window.location.href = "{catalog_url}";
1027
- </script>
1028
- <p>Вход выполнен успешно. Перенаправление в <a href="{catalog_url}">каталог</a>...</p>
1029
- </body></html>
1030
- '''
1031
-
1032
  ORDER_TEMPLATE = '''
1033
  <!DOCTYPE html>
1034
  <html lang="ru">
@@ -1057,7 +869,7 @@ ORDER_TEMPLATE = '''
1057
  .customer-info p { margin-bottom: 8px; font-size: 0.95rem; }
1058
  .customer-info strong { color: #164B41; }
1059
  .actions { margin-top: 30px; text-align: center; }
1060
- .button { padding: 12px 25px; border: none; border-radius: 8px; background-color: #25D366; /* WhatsApp Green */ color: white; font-weight: 600; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; font-size: 1rem; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; }
1061
  .button:hover { background-color: #128C7E; }
1062
  .button:active { transform: scale(0.98); }
1063
  .button i { font-size: 1.2rem; }
@@ -1090,25 +902,14 @@ ORDER_TEMPLATE = '''
1090
 
1091
  <div class="order-summary">
1092
  <p>Общая сумма товаров: <strong>{{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
1093
- {# Add shipping/discount info here if applicable in the future #}
1094
  <p><strong>ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
1095
  </div>
1096
 
1097
- {% if order.user_info %}
1098
  <div class="customer-info">
1099
- <h2><i class="fas fa-user"></i> Информация о клиенте</h2>
1100
- <p><strong>Логин:</strong> {{ order.user_info.login }}</p>
1101
- <p><strong>Имя:</strong> {{ order.user_info.get('first_name', '') }} {{ order.user_info.get('last_name', '') }}</p>
1102
- <p><strong>Телефон:</strong> {{ order.user_info.get('phone', 'Не указан') }}</p>
1103
- <p><strong>Страна:</strong> {{ order.user_info.get('country', 'Не указана') }}</p>
1104
- <p><strong>Город:</strong> {{ order.user_info.get('city', 'Не указан') }}</p>
1105
  </div>
1106
- {% else %}
1107
- <div class="customer-info">
1108
- <h2><i class="fas fa-user-slash"></i> Информация о клиенте</h2>
1109
- <p>Клиент не был авторизован при создании заказа.</p>
1110
- </div>
1111
- {% endif %}
1112
 
1113
  <div class="actions">
1114
  <button class="button" onclick="sendOrderViaWhatsApp()"><i class="fab fa-whatsapp"></i> Отправить заказ</button>
@@ -1119,8 +920,8 @@ ORDER_TEMPLATE = '''
1119
  <script>
1120
  function sendOrderViaWhatsApp() {
1121
  const orderId = '{{ order.id }}';
1122
- const orderUrl = `{{ request.url }}`; // Gets the current page URL
1123
- const whatsappNumber = "996997703090"; // Replace with your number
1124
 
1125
  let message = `Здравствуйте! Хочу подтвердить свой заказ на Soola Cosmetics:%0A%0A`;
1126
  message += `*Номер заказа:* ${orderId}%0A`;
@@ -1242,7 +1043,6 @@ ADMIN_TEMPLATE = '''
1242
  <p style="font-size: 0.85rem; color: #5e6e68;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
1243
  </div>
1244
 
1245
-
1246
  <div class="flex-container">
1247
  <div class="flex-item">
1248
  <div class="section">
@@ -1281,59 +1081,13 @@ ADMIN_TEMPLATE = '''
1281
 
1282
  <div class="flex-item">
1283
  <div class="section">
1284
- <h2><i class="fas fa-users"></i> Управление пользователями</h2>
1285
- <details>
1286
- <summary><i class="fas fa-user-plus"></i> Добавить нового пользователя</summary>
1287
- <div class="form-content">
1288
- <form method="POST">
1289
- <input type="hidden" name="action" value="add_user">
1290
- <label for="login">Логин *:</label>
1291
- <input type="text" id="login" name="login" required>
1292
- <label for="password">Пароль *:</label>
1293
- <input type="password" id="password" name="password" required title="Пароль будет сохранен в открытом виде.">
1294
- <p style="font-size: 0.8rem; color: #777;">Логин и пароль обязательны.</p>
1295
- <label for="first_name">Имя:</label>
1296
- <input type="text" id="first_name" name="first_name">
1297
- <label for="last_name">Фамилия:</label>
1298
- <input type="text" id="last_name" name="last_name">
1299
- <label for="phone">Телефон:</label>
1300
- <input type="tel" id="phone" name="phone">
1301
- <label for="country">Страна:</label>
1302
- <input type="text" id="country" name="country">
1303
- <label for="city">Город:</label>
1304
- <input type="text" id="city" name="city">
1305
- <button type="submit" class="add-button"><i class="fas fa-save"></i> Сохранить пользователя</button>
1306
- </form>
1307
- </div>
1308
- </details>
1309
-
1310
- <h3>Список пользователей:</h3>
1311
- {% if users %}
1312
- <div class="item-list">
1313
- {% for login, user_data in users.items() %}
1314
- <div class="item">
1315
- <p><strong>Логин:</strong> {{ login }}</p>
1316
- <p><strong>Имя:</strong> {{ user_data.get('first_name', 'N/A') }} {{ user_data.get('last_name', '') }}</p>
1317
- <p><strong>Телефон:</strong> {{ user_data.get('phone', 'N/A') }}</p>
1318
- <p><strong>Локация:</strong> {{ user_data.get('city', 'N/A') }}, {{ user_data.get('country', 'N/A') }}</p>
1319
- <div class="item-actions">
1320
- <form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить пользователя \'{{ login }}\'?');">
1321
- <input type="hidden" name="action" value="delete_user">
1322
- <input type="hidden" name="login" value="{{ login }}">
1323
- <button type="submit" class="delete-button"><i class="fas fa-user-slash"></i> Удалить</button>
1324
- </form>
1325
- </div>
1326
- </div>
1327
- {% endfor %}
1328
- </div>
1329
- {% else %}
1330
- <p>Пользователей пока нет.</p>
1331
- {% endif %}
1332
  </div>
1333
  </div>
1334
  </div>
1335
 
1336
-
1337
  <div class="section">
1338
  <h2><i class="fas fa-box-open"></i> Управление товарами</h2>
1339
  <details>
@@ -1527,7 +1281,6 @@ ADMIN_TEMPLATE = '''
1527
  if (group) {
1528
  const container = group.parentNode;
1529
  group.remove();
1530
- // Add a placeholder input if the container becomes empty
1531
  if (container && container.children.length === 0) {
1532
  const placeholderGroup = document.createElement('div');
1533
  placeholderGroup.className = 'color-input-group';
@@ -1552,10 +1305,8 @@ ADMIN_TEMPLATE = '''
1552
  def catalog():
1553
  data = load_data()
1554
  all_products = data.get('products', [])
1555
- categories = sorted(data.get('categories', [])) # Sort categories alphabetically
1556
- is_authenticated = 'user' in session
1557
 
1558
- # Filter out-of-stock and sort: top first, then by name
1559
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1560
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
1561
 
@@ -1564,9 +1315,7 @@ def catalog():
1564
  products=products_sorted,
1565
  categories=categories,
1566
  repo_id=REPO_ID,
1567
- is_authenticated=is_authenticated,
1568
  store_address=STORE_ADDRESS,
1569
- session=session,
1570
  currency_code=CURRENCY_CODE
1571
  )
1572
 
@@ -1574,14 +1323,11 @@ def catalog():
1574
  def product_detail(index):
1575
  data = load_data()
1576
  all_products = data.get('products', [])
1577
- # We need the same filtering and sorting logic as in catalog to match the index
1578
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1579
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
1580
 
1581
- is_authenticated = 'user' in session
1582
  try:
1583
  product = products_sorted[index]
1584
- # No need to check in_stock again, already filtered
1585
  except IndexError:
1586
  logging.warning(f"Attempted access to non-existent or out-of-stock product with index {index}")
1587
  return "Товар не найден или отсутствует в наличии.", 404
@@ -1590,85 +1336,9 @@ def product_detail(index):
1590
  PRODUCT_DETAIL_TEMPLATE,
1591
  product=product,
1592
  repo_id=REPO_ID,
1593
- is_authenticated=is_authenticated,
1594
  currency_code=CURRENCY_CODE
1595
  )
1596
 
1597
- @app.route('/login', methods=['GET', 'POST'])
1598
- def login():
1599
- if request.method == 'POST':
1600
- login_attempt = request.form.get('login')
1601
- password = request.form.get('password')
1602
- if not login_attempt or not password:
1603
- return render_template_string(LOGIN_TEMPLATE, error="Логин и пароль не могут быть пустыми."), 400
1604
-
1605
- users = load_users()
1606
-
1607
- if login_attempt in users and users[login_attempt].get('password') == password:
1608
- user_info = users[login_attempt]
1609
- session['user'] = login_attempt
1610
- session['user_info'] = {
1611
- 'login': login_attempt,
1612
- 'first_name': user_info.get('first_name', ''),
1613
- 'last_name': user_info.get('last_name', ''),
1614
- 'country': user_info.get('country', ''),
1615
- 'city': user_info.get('city', ''),
1616
- 'phone': user_info.get('phone', '')
1617
- }
1618
- logging.info(f"User {login_attempt} logged in successfully.")
1619
- # Use template for redirect script
1620
- return render_template_string(LOGIN_REDIRECT_TEMPLATE,
1621
- login=login_attempt,
1622
- catalog_url=url_for('catalog'))
1623
- else:
1624
- logging.warning(f"Failed login attempt for user {login_attempt}.")
1625
- error_message = "Неверный логин или пароль."
1626
- return render_template_string(LOGIN_TEMPLATE, error=error_message), 401
1627
-
1628
- # GET request
1629
- return render_template_string(LOGIN_TEMPLATE, error=None)
1630
-
1631
- @app.route('/auto_login', methods=['POST'])
1632
- def auto_login():
1633
- data = request.get_json()
1634
- if not data or 'login' not in data:
1635
- logging.warning("Auto_login request missing data or login.")
1636
- return jsonify({"error": "Invalid request"}), 400
1637
-
1638
- login_attempt = data.get('login')
1639
- if not login_attempt:
1640
- logging.warning("Attempted auto_login with empty login.")
1641
- return jsonify({"error": "Login not provided"}), 400
1642
-
1643
- users = load_users()
1644
- if login_attempt in users:
1645
- user_info = users[login_attempt]
1646
- session['user'] = login_attempt
1647
- session['user_info'] = {
1648
- 'login': login_attempt,
1649
- 'first_name': user_info.get('first_name', ''),
1650
- 'last_name': user_info.get('last_name', ''),
1651
- 'country': user_info.get('country', ''),
1652
- 'city': user_info.get('city', ''),
1653
- 'phone': user_info.get('phone', '')
1654
- }
1655
- logging.info(f"Auto-login successful for user {login_attempt}.")
1656
- return jsonify({"message": "OK"}), 200
1657
- else:
1658
- logging.warning(f"Failed auto-login attempt for non-existent user {login_attempt}.")
1659
- # Don't reveal user existence, just fail generically
1660
- return jsonify({"error": "Auto-login failed"}), 401 # Use 401 Unauthorized
1661
-
1662
- @app.route('/logout')
1663
- def logout():
1664
- logged_out_user = session.get('user')
1665
- session.pop('user', None)
1666
- session.pop('user_info', None)
1667
- if logged_out_user:
1668
- logging.info(f"User {logged_out_user} logged out.")
1669
- # Use template for redirect script
1670
- return render_template_string(LOGOUT_REDIRECT_TEMPLATE)
1671
-
1672
  @app.route('/create_order', methods=['POST'])
1673
  def create_order():
1674
  order_data = request.get_json()
@@ -1678,13 +1348,10 @@ def create_order():
1678
  return jsonify({"error": "Корзина пуста или не передана."}), 400
1679
 
1680
  cart_items = order_data['cart']
1681
- user_info = order_data.get('userInfo', None) # User info might be null if not logged in
1682
 
1683
- # Recalculate total server-side for security/consistency
1684
  total_price = 0
1685
  processed_cart = []
1686
  for item in cart_items:
1687
- # Basic validation (can be expanded)
1688
  if not all(k in item for k in ('name', 'price', 'quantity')):
1689
  logging.error(f"Invalid cart item structure received: {item}")
1690
  return jsonify({"error": "Неверный формат товара в корзине."}), 400
@@ -1694,14 +1361,12 @@ def create_order():
1694
  if price < 0 or quantity <= 0:
1695
  raise ValueError("Invalid price or quantity")
1696
  total_price += price * quantity
1697
- # Construct item data for saving
1698
  processed_cart.append({
1699
  "name": item['name'],
1700
  "price": price,
1701
  "quantity": quantity,
1702
  "color": item.get('color', 'N/A'),
1703
- "photo": item.get('photo'), # Store photo filename if available
1704
- # Generate photo URL here for easier display later
1705
  "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/60x60.png?text=N/A"
1706
  })
1707
  except (ValueError, TypeError) as e:
@@ -1716,20 +1381,19 @@ def create_order():
1716
  "created_at": order_timestamp,
1717
  "cart": processed_cart,
1718
  "total_price": round(total_price, 2),
1719
- "user_info": user_info, # Can be None
1720
- "status": "new" # Initial status
1721
  }
1722
 
1723
  try:
1724
  data = load_data()
1725
- # Ensure 'orders' key exists and is a dictionary
1726
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1727
  data['orders'] = {}
1728
 
1729
  data['orders'][order_id] = new_order
1730
  save_data(data)
1731
- logging.info(f"Order {order_id} created successfully.")
1732
- return jsonify({"order_id": order_id}), 201 # 201 Created status code
1733
 
1734
  except Exception as e:
1735
  logging.error(f"Failed to save order {order_id}: {e}", exc_info=True)
@@ -1737,13 +1401,10 @@ def create_order():
1737
 
1738
  @app.route('/order/<order_id>')
1739
  def view_order(order_id):
1740
- # No login required for this route
1741
  data = load_data()
1742
  order = data.get('orders', {}).get(order_id)
1743
 
1744
  if order:
1745
- # Format timestamp for display if needed
1746
- # order['created_at_formatted'] = ...
1747
  logging.info(f"Displaying order {order_id}")
1748
  else:
1749
  logging.warning(f"Order {order_id} not found.")
@@ -1753,22 +1414,11 @@ def view_order(order_id):
1753
  repo_id=REPO_ID,
1754
  currency_code=CURRENCY_CODE)
1755
 
1756
-
1757
  @app.route('/admin', methods=['GET', 'POST'])
1758
  def admin():
1759
- # Simple admin check (replace with proper authentication/authorization)
1760
- # For now, allow anyone access for demonstration, add security later
1761
- # if 'user' not in session or session.get('user') != 'admin_user':
1762
- # flash("Доступ запрещен.", "error")
1763
- # return redirect(url_for('login'))
1764
-
1765
- # Load data and users *before* processing POST to have the current state
1766
  data = load_data()
1767
- users = load_users()
1768
- # Ensure products and categories are lists/dicts even if load failed
1769
  products = data.get('products', [])
1770
  categories = data.get('categories', [])
1771
- # Ensure 'orders' key exists and is a dictionary
1772
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1773
  data['orders'] = {}
1774
 
@@ -1781,8 +1431,7 @@ def admin():
1781
  category_name = request.form.get('category_name', '').strip()
1782
  if category_name and category_name not in categories:
1783
  categories.append(category_name)
1784
- # categories.sort() # Keep sorted list
1785
- data['categories'] = categories # Update the main data dict
1786
  save_data(data)
1787
  logging.info(f"Category '{category_name}' added.")
1788
  flash(f"Категория '{category_name}' успешно добавлена.", 'success')
@@ -1798,13 +1447,12 @@ def admin():
1798
  if category_to_delete and category_to_delete in categories:
1799
  categories.remove(category_to_delete)
1800
  updated_count = 0
1801
- # Update products that used this category
1802
  for product in products:
1803
  if product.get('category') == category_to_delete:
1804
  product['category'] = 'Без категории'
1805
  updated_count += 1
1806
- data['categories'] = categories # Update the main data dict
1807
- data['products'] = products # Update affected products
1808
  save_data(data)
1809
  logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
1810
  flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
@@ -1853,8 +1501,7 @@ def admin():
1853
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1854
  continue
1855
 
1856
- # Create a unique filename based on product name and time
1857
- safe_name = secure_filename(name.replace(' ', '_'))[:50] # Limit length
1858
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1859
  temp_path = os.path.join(uploads_dir, photo_filename)
1860
  photo.save(temp_path)
@@ -1869,18 +1516,17 @@ def admin():
1869
  )
1870
  photos_list.append(photo_filename)
1871
  logging.info(f"Photo {photo_filename} uploaded successfully.")
1872
- os.remove(temp_path) # Clean up temp file
1873
  uploaded_count += 1
1874
  except Exception as e:
1875
  logging.error(f"Error uploading photo {photo.filename} to HF: {e}", exc_info=True)
1876
  flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
1877
  if os.path.exists(temp_path):
1878
  try: os.remove(temp_path)
1879
- except OSError: pass # Ignore error if file cannot be removed
1880
  elif photo and not photo.filename:
1881
  logging.warning("Received an empty photo file object when adding product.")
1882
  try:
1883
- # Clean up temp dir if empty
1884
  if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
1885
  os.rmdir(uploads_dir)
1886
  except OSError as e:
@@ -1890,14 +1536,13 @@ def admin():
1890
 
1891
 
1892
  new_product = {
1893
- # Consider adding a unique ID here later if needed
1894
  'name': name, 'price': price, 'description': description,
1895
  'category': category if category in categories else 'Без категории',
1896
  'photos': photos_list, 'colors': colors,
1897
  'in_stock': in_stock, 'is_top': is_top
1898
  }
1899
  products.append(new_product)
1900
- data['products'] = products # Update the main data dict
1901
  save_data(data)
1902
  logging.info(f"Product '{name}' added.")
1903
  flash(f"Товар '{name}' успешно добавлен.", 'success')
@@ -1910,7 +1555,6 @@ def admin():
1910
 
1911
  try:
1912
  index = int(index_str)
1913
- # Use the 'products' list loaded at the start of the function
1914
  if not (0 <= index < len(products)):
1915
  raise IndexError("Product index out of range")
1916
  product_to_edit = products[index]
@@ -1921,7 +1565,6 @@ def admin():
1921
  logging.error(f"Invalid index '{index_str}' for editing. Product list length: {len(products)}")
1922
  return redirect(url_for('admin'))
1923
 
1924
- # Update fields
1925
  product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
1926
  price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
1927
  product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
@@ -1939,7 +1582,6 @@ def admin():
1939
  logging.warning(f"Invalid price format '{price_str}' during edit of product {original_name}. Price not changed.")
1940
  flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
1941
 
1942
- # Handle photo replacement
1943
  photos_files = request.files.getlist('photos')
1944
  if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE:
1945
  uploads_dir = 'uploads_temp'
@@ -1988,7 +1630,6 @@ def admin():
1988
 
1989
  if new_photos_list:
1990
  logging.info(f"New photo list for product {product_to_edit['name']} generated.")
1991
- # Delete old photos from HF *after* new ones are uploaded
1992
  old_photos = product_to_edit.get('photos', [])
1993
  if old_photos:
1994
  logging.info(f"Attempting to delete old photos: {old_photos}")
@@ -2002,21 +1643,17 @@ def admin():
2002
  )
2003
  logging.info(f"Old photos for product {product_to_edit['name']} deleted from HF.")
2004
  except Exception as e:
2005
- # Log error but continue, new photos are already uploaded
2006
  logging.error(f"Error deleting old photos {old_photos} from HF: {e}", exc_info=True)
2007
  flash("Не удалось удалить старые фотографии с сервера. Новые фото загружены.", "warning")
2008
- # Update product data with new photo list
2009
  product_to_edit['photos'] = new_photos_list
2010
  flash("Фотографии товара успешно обновлены.", "success")
2011
  elif uploaded_count == 0 and any(f.filename for f in photos_files):
2012
- # Files were selected, but none were uploaded (e.g., wrong format)
2013
  flash("Не удалось загрузить новые фотографии (возможно, неверный формат).", "error")
2014
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
2015
  flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning")
2016
 
2017
- # Update the product in the main list
2018
  products[index] = product_to_edit
2019
- data['products'] = products # Update the main data dict
2020
  save_data(data)
2021
  logging.info(f"Product '{original_name}' (index {index}) updated to '{product_to_edit['name']}'.")
2022
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
@@ -2028,12 +1665,10 @@ def admin():
2028
  return redirect(url_for('admin'))
2029
  try:
2030
  index = int(index_str)
2031
- # Use the 'products' list loaded at the start of the function
2032
  if not (0 <= index < len(products)): raise IndexError("Product index out of range")
2033
  deleted_product = products.pop(index)
2034
  product_name = deleted_product.get('name', 'N/A')
2035
 
2036
- # Attempt to delete associated photos from HF
2037
  photos_to_delete = deleted_product.get('photos', [])
2038
  if photos_to_delete and HF_TOKEN_WRITE:
2039
  logging.info(f"Attempting to delete photos for product '{product_name}' from HF: {photos_to_delete}")
@@ -2048,7 +1683,6 @@ def admin():
2048
  )
2049
  logging.info(f"Photos for product '{product_name}' deleted from HF.")
2050
  except Exception as e:
2051
- # Log error but proceed with local deletion
2052
  logging.error(f"Error deleting photos {photos_to_delete} for product '{product_name}' from HF: {e}", exc_info=True)
2053
  flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning")
2054
  elif photos_to_delete and not HF_TOKEN_WRITE:
@@ -2056,7 +1690,7 @@ def admin():
2056
  flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
2057
 
2058
 
2059
- data['products'] = products # Update the main data dict
2060
  save_data(data)
2061
  logging.info(f"Product '{product_name}' (original index {index}) deleted.")
2062
  flash(f"Товар '{product_name}' удален.", 'success')
@@ -2064,84 +1698,35 @@ def admin():
2064
  flash(f"Ошибка удаления: неверный индекс товара '{index_str}'.", 'error')
2065
  logging.error(f"Invalid index '{index_str}' for deletion. Product list length: {len(products)}")
2066
 
2067
- elif action == 'add_user':
2068
- login = request.form.get('login', '').strip()
2069
- password = request.form.get('password', '').strip() # Storing plain text password
2070
- first_name = request.form.get('first_name', '').strip()
2071
- last_name = request.form.get('last_name', '').strip()
2072
- phone = request.form.get('phone', '').strip()
2073
- country = request.form.get('country', '').strip()
2074
- city = request.form.get('city', '').strip()
2075
-
2076
- if not login or not password:
2077
- flash("Логин и пароль пользователя обязательны.", 'error')
2078
- return redirect(url_for('admin'))
2079
- if login in users:
2080
- flash(f"Пользователь с логином '{login}' уже существует.", 'error')
2081
- return redirect(url_for('admin'))
2082
-
2083
- users[login] = {
2084
- 'password': password, # WARNING: Storing plain text
2085
- 'first_name': first_name, 'last_name': last_name,
2086
- 'phone': phone,
2087
- 'country': country, 'city': city
2088
- }
2089
- save_users(users)
2090
- logging.info(f"User '{login}' added.")
2091
- flash(f"Пользователь '{login}' успешно добавлен.", 'success')
2092
-
2093
- elif action == 'delete_user':
2094
- login_to_delete = request.form.get('login')
2095
- if login_to_delete and login_to_delete in users:
2096
- # Optional: Prevent deleting the current admin user?
2097
- # if login_to_delete == session.get('user'):
2098
- # flash("Нельзя удалить текущего пользователя.", 'error')
2099
- # return redirect(url_for('admin'))
2100
- del users[login_to_delete]
2101
- save_users(users)
2102
- logging.info(f"User '{login_to_delete}' deleted.")
2103
- flash(f"Пользователь '{login_to_delete}' удален.", 'success')
2104
- else:
2105
- logging.warning(f"Attempted to delete non-existent or empty user: {login_to_delete}")
2106
- flash(f"Не удалось удалить пользователя '{login_to_delete}'.", 'error')
2107
-
2108
  else:
2109
  logging.warning(f"Received unknown admin action: {action}")
2110
  flash(f"Неизвестное действие: {action}", 'warning')
2111
 
2112
- # Redirect after POST to prevent form resubmission
2113
  return redirect(url_for('admin'))
2114
 
2115
  except Exception as e:
2116
- # Catch potential errors during file operations, HF API calls, etc.
2117
  logging.error(f"Error processing admin action '{action}': {e}", exc_info=True)
2118
  flash(f"Произошла внутренняя ошибка при выполнении дейс��вия '{action}'. Подробности в логе сервера.", 'error')
2119
  return redirect(url_for('admin'))
2120
 
2121
  # --- GET request ---
2122
- # Reload data and users again to ensure the template shows the latest state
2123
  current_data = load_data()
2124
- current_users = load_users()
2125
- # Sort products for consistent display in admin panel (e.g., by name)
2126
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
2127
  display_categories = sorted(current_data.get('categories', []))
2128
- display_users = dict(sorted(current_users.items())) # Sort users by login
2129
 
2130
  return render_template_string(
2131
  ADMIN_TEMPLATE,
2132
  products=display_products,
2133
  categories=display_categories,
2134
- users=display_users,
2135
  repo_id=REPO_ID,
2136
  currency_code=CURRENCY_CODE
2137
  )
2138
 
2139
  @app.route('/force_upload', methods=['POST'])
2140
  def force_upload():
2141
- # Add admin check here if needed
2142
  logging.info("Forcing upload to Hugging Face...")
2143
  try:
2144
- upload_db_to_hf() # Uploads both files by default
2145
  flash("Данные успешно загружены на Hugging Face.", 'success')
2146
  except Exception as e:
2147
  logging.error(f"Error during forced upload: {e}", exc_info=True)
@@ -2150,14 +1735,11 @@ def force_upload():
2150
 
2151
  @app.route('/force_download', methods=['POST'])
2152
  def force_download():
2153
- # Add admin check here if needed
2154
  logging.info("Forcing download from Hugging Face...")
2155
  try:
2156
- if download_db_from_hf(): # Downloads both files by default
2157
  flash("Данные успешно скачаны с Hugging Face. Локальные файлы обновлены.", 'success')
2158
- # Reload data/users in memory after download might be needed depending on app structure
2159
- # load_data()
2160
- # load_users()
2161
  else:
2162
  flash("Не удалось скачать данные с Hugging Face после нескольких попыток. Проверьте логи.", 'error')
2163
  except Exception as e:
@@ -2169,14 +1751,11 @@ def force_download():
2169
  # --- App Initialization ---
2170
 
2171
  if __name__ == '__main__':
2172
- # Initial download/load on startup
2173
  logging.info("Application starting up. Performing initial data load/download...")
2174
- download_db_from_hf() # Attempt to download both files first
2175
- load_data() # Load data (will use downloaded or default)
2176
- load_users() # Load users (will use downloaded or default)
2177
  logging.info("Initial data load complete.")
2178
 
2179
-
2180
  if HF_TOKEN_WRITE:
2181
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
2182
  backup_thread.start()
@@ -2186,9 +1765,6 @@ if __name__ == '__main__':
2186
 
2187
  port = int(os.environ.get('PORT', 7860))
2188
  logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
2189
- # Use waitress or gunicorn in production instead of development server
2190
- # For simplicity, using Flask's dev server here (debug=False for production-like behavior)
2191
- # Consider using waitress: pip install waitress; waitress-serve --host=0.0.0.0 --port=7860 app:app
2192
  app.run(debug=False, host='0.0.0.0', port=port)
2193
 
2194
  # --- END OF FILE app.py ---
 
1
+
2
  # --- START OF FILE app.py ---
3
 
4
+ from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify
5
  import json
6
  import os
7
  import logging
 
12
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
13
  from werkzeug.utils import secure_filename
14
  from dotenv import load_dotenv
15
+ import requests
16
+ import uuid
17
 
18
  load_dotenv()
19
 
20
  app = Flask(__name__)
21
+ app.secret_key = 'your_unique_secret_key_soola_cosmetics_67890_no_login' # Secret key still needed for flash messages
22
+ DATA_FILE = 'data.json'
23
+
24
 
25
+ SYNC_FILES = [DATA_FILE]
26
 
27
+ REPO_ID = "Kgshop/medinaturkey"
28
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
29
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
30
 
 
41
  # --- Hugging Face Sync Functions ---
42
 
43
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
44
+ if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
45
  logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
 
46
 
47
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
48
 
 
63
  local_dir=".",
64
  local_dir_use_symlinks=False,
65
  force_download=True,
66
+ resume_download=False
67
  )
68
  logging.info(f"Successfully downloaded {file_name} to {local_path}.")
69
  success = True
70
+ break
71
  except RepositoryNotFoundError:
72
  logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.")
73
+ return False
74
  except HfHubHTTPError as e:
75
  if e.response.status_code == 404:
76
  logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Skipping this file.")
 
77
  if attempt == 0 and not os.path.exists(file_name):
78
  try:
79
  if file_name == DATA_FILE:
80
  with open(file_name, 'w', encoding='utf-8') as f:
81
  json.dump({'products': [], 'categories': [], 'orders': {}}, f)
82
  logging.info(f"Created empty local file {file_name} because it was not found on HF.")
 
 
 
 
83
  except Exception as create_e:
84
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
85
+ success = False
86
+ break
87
  else:
88
  logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
89
+ except requests.exceptions.RequestException as e:
90
  logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
91
  except Exception as e:
92
  logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
 
96
 
97
  if not success:
98
  logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
99
+ all_successful = False
100
 
101
  logging.info(f"Download process finished. Overall success: {all_successful}")
102
  return all_successful
 
149
  logging.info(f"Local data loaded successfully from {DATA_FILE}")
150
  if not isinstance(data, dict):
151
  logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
152
+ raise FileNotFoundError
153
  if 'products' not in data: data['products'] = []
154
  if 'categories' not in data: data['categories'] = []
155
+ if 'orders' not in data: data['orders'] = {}
156
  return data
157
  except FileNotFoundError:
158
  logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
159
  except json.JSONDecodeError:
160
  logging.error(f"Error decoding JSON in local {DATA_FILE}. File might be corrupt. Attempting download.")
161
 
 
162
  if download_db_from_hf(specific_file=DATA_FILE):
163
  try:
 
164
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
165
  data = json.load(file)
166
  logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
 
169
  return default_data
170
  if 'products' not in data: data['products'] = []
171
  if 'categories' not in data: data['categories'] = []
172
+ if 'orders' not in data: data['orders'] = {}
173
  return data
174
  except FileNotFoundError:
175
  logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.")
 
182
  return default_data
183
  else:
184
  logging.error(f"Failed to download {DATA_FILE} from HF after retries. Using empty default data structure.")
 
185
  if not os.path.exists(DATA_FILE):
186
  try:
187
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
 
189
  logging.info(f"Created empty local file {DATA_FILE} after failed download.")
190
  except Exception as create_e:
191
  logging.error(f"Failed to create empty local file {DATA_FILE}: {create_e}")
192
+ return default_data
193
 
194
  def save_data(data):
195
  try:
 
196
  if not isinstance(data, dict):
197
  logging.error("Attempted to save invalid data structure (not a dict). Aborting save.")
198
  return
199
  if 'products' not in data: data['products'] = []
200
  if 'categories' not in data: data['categories'] = []
201
+ if 'orders' not in data: data['orders'] = {}
202
 
203
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
204
  json.dump(data, file, ensure_ascii=False, indent=4)
 
207
  except Exception as e:
208
  logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  # --- Templates ---
211
 
212
  CATALOG_TEMPLATE = '''
 
227
  .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #d1e7dd; }
228
  body.dark-mode .header { border-bottom-color: #2c4a41; }
229
  .header h1 { font-size: 1.8rem; font-weight: 600; color: #1C6758; }
 
 
 
 
 
 
230
  .theme-toggle { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #7a8d85; transition: color 0.3s ease; }
231
  .theme-toggle:hover { color: #3D8361; }
232
  body.dark-mode .theme-toggle { color: #8aa39a; }
 
253
  body.dark-mode .product:hover { box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); }
254
  .product-image { width: 100%; aspect-ratio: 1 / 1; background-color: #fff; border-radius: 10px 10px 0 0; overflow: hidden; display: flex; justify-content: center; align-items: center; margin-bottom: 0; }
255
  .product-image img { max-width: 100%; max-height: 100%; object-fit: contain; transition: transform 0.3s ease; }
 
256
  .product-info { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; justify-content: center; }
257
  .product h2 { font-size: 1.1rem; font-weight: 600; margin: 0 0 8px 0; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #2d332f; }
258
  body.dark-mode .product h2 { color: #c8d8d3; }
 
313
  <div class="container">
314
  <div class="header">
315
  <h1>MedinaTurkey.kz</h1>
 
 
 
 
 
 
 
 
316
  <button class="theme-toggle" onclick="toggleTheme()" aria-label="Переключить тему">
317
  <i class="fas fa-moon"></i>
318
  </button>
 
351
  </div>
352
  <div class="product-info">
353
  <h2>{{ product['name'] }}</h2>
 
354
  <div class="product-price">{{ "%.2f"|format(product['price']) }} {{ currency_code }}</div>
 
 
 
355
  <p class="product-description">{{ product.get('description', '')[:50] }}{% if product.get('description', '')|length > 50 %}...{% endif %}</p>
356
  </div>
357
  <div class="product-actions">
358
  <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
 
359
  <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">
360
  <i class="fas fa-cart-plus"></i> В корзину
361
  </button>
 
362
  </div>
363
  </div>
364
  {% endfor %}
 
418
  const products = {{ products|tojson }};
419
  const repoId = '{{ repo_id }}';
420
  const currencyCode = '{{ currency_code }}';
 
 
421
  let selectedProductIndex = null;
422
  let cart = JSON.parse(localStorage.getItem('soolaCart') || '[]');
423
 
 
439
  }
440
  }
441
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  function openModal(index) {
443
  loadProductDetails(index);
444
  const modal = document.getElementById('productModal');
 
495
  }
496
 
497
  function openQuantityModal(index) {
 
 
 
 
 
498
  selectedProductIndex = index;
499
  const product = products[index];
500
  if (!product) {
 
558
  cart[existingItemIndex].quantity += quantity;
559
  } else {
560
  cart.push({
561
+ id: cartItemId,
562
  name: product.name,
563
  price: product.price,
564
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
 
633
  function removeFromCart(itemId) {
634
  cart = cart.filter(item => item.id !== itemId);
635
  localStorage.setItem('soolaCart', JSON.stringify(cart));
636
+ openCartModal();
637
  updateCartButton();
638
  }
639
 
 
641
  if (confirm("Вы уверены, что хотите очистить корзину?")) {
642
  cart = [];
643
  localStorage.removeItem('soolaCart');
644
+ openCartModal();
645
  updateCartButton();
646
  }
647
  }
 
653
  }
654
 
655
  const orderData = {
656
+ cart: cart
 
657
  };
658
 
 
659
  const formulateButton = document.querySelector('.formulate-order-button');
660
  if (formulateButton) formulateButton.disabled = true;
661
 
 
674
  })
675
  .then(data => {
676
  if (data.order_id) {
677
+ localStorage.removeItem('soolaCart');
678
+ cart = [];
679
+ updateCartButton();
680
+ closeModal('cartModal');
681
+ window.location.href = `/order/${data.order_id}`;
682
  } else {
683
  throw new Error('Не получен ID заказа от сервера.');
684
  }
 
686
  .catch(error => {
687
  console.error('Ошибка при формировании заказа:', error);
688
  alert(`Ошибка: ${error.message}`);
689
+ if (formulateButton) formulateButton.disabled = false;
690
  });
691
  }
692
 
 
743
  filterProducts();
744
  });
745
  });
746
+ filterProducts();
747
  }
748
 
749
  function showNotification(message, duration = 3000) {
750
  const placeholder = document.getElementById('notification-placeholder');
751
  if (!placeholder) {
 
752
  const newPlaceholder = document.createElement('div');
753
  newPlaceholder.id = 'notification-placeholder';
754
  newPlaceholder.style.position = 'fixed';
755
+ newPlaceholder.style.bottom = '80px';
756
  newPlaceholder.style.left = '50%';
757
  newPlaceholder.style.transform = 'translateX(-50%)';
758
  newPlaceholder.style.zIndex = '1002';
 
766
  notification.textContent = message;
767
  placeholder.appendChild(notification);
768
 
 
769
  void notification.offsetWidth;
770
 
771
  notification.classList.add('show');
772
 
773
  setTimeout(() => {
774
  notification.classList.remove('show');
 
775
  notification.addEventListener('transitionend', () => notification.remove());
776
  }, duration);
777
  }
778
 
779
  document.addEventListener('DOMContentLoaded', () => {
780
  applyInitialTheme();
 
781
  updateCartButton();
782
  setupFilters();
783
 
 
784
  window.addEventListener('click', function(event) {
785
  if (event.target.classList.contains('modal')) {
786
  closeModal(event.target.id);
787
  }
788
  });
789
 
 
790
  window.addEventListener('keydown', function(event) {
791
  if (event.key === 'Escape') {
792
  document.querySelectorAll('.modal[style*="display: block"]').forEach(modal => {
 
831
 
832
  <div style="margin-top: 20px; font-size: 1rem; line-height: 1.7;">
833
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
834
+ <p style="font-size: 1.2rem; font-weight: bold; color: #1C6758;"><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
 
 
 
 
835
  <p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\\n', '<br>')|safe }}</p>
836
  {% set colors = product.get('colors', []) %}
837
  {% if colors and colors|select('ne', '')|list|length > 0 %}
 
841
  </div>
842
  '''
843
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
844
  ORDER_TEMPLATE = '''
845
  <!DOCTYPE html>
846
  <html lang="ru">
 
869
  .customer-info p { margin-bottom: 8px; font-size: 0.95rem; }
870
  .customer-info strong { color: #164B41; }
871
  .actions { margin-top: 30px; text-align: center; }
872
+ .button { padding: 12px 25px; border: none; border-radius: 8px; background-color: #25D366; color: white; font-weight: 600; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; font-size: 1rem; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; }
873
  .button:hover { background-color: #128C7E; }
874
  .button:active { transform: scale(0.98); }
875
  .button i { font-size: 1.2rem; }
 
902
 
903
  <div class="order-summary">
904
  <p>Общая сумма товаров: <strong>{{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
 
905
  <p><strong>ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
906
  </div>
907
 
 
908
  <div class="customer-info">
909
+ <h2><i class="fas fa-info-circle"></i> Статус заказа</h2>
910
+ <p>Этот заказ был оформлен без входа в систему.</p>
911
+ <p>Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.</p>
 
 
 
912
  </div>
 
 
 
 
 
 
913
 
914
  <div class="actions">
915
  <button class="button" onclick="sendOrderViaWhatsApp()"><i class="fab fa-whatsapp"></i> Отправить заказ</button>
 
920
  <script>
921
  function sendOrderViaWhatsApp() {
922
  const orderId = '{{ order.id }}';
923
+ const orderUrl = `{{ request.url }}`;
924
+ const whatsappNumber = "77479003212";
925
 
926
  let message = `Здравствуйте! Хочу подтвердить свой заказ на Soola Cosmetics:%0A%0A`;
927
  message += `*Номер заказа:* ${orderId}%0A`;
 
1043
  <p style="font-size: 0.85rem; color: #5e6e68;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
1044
  </div>
1045
 
 
1046
  <div class="flex-container">
1047
  <div class="flex-item">
1048
  <div class="section">
 
1081
 
1082
  <div class="flex-item">
1083
  <div class="section">
1084
+ <h2><i class="fas fa-info-circle"></i> Информация</h2>
1085
+ <p>Управление пользователями отключено, так как сайт не требует входа.</p>
1086
+ <p>Заказы создаются анонимно и должны быть подтверждены через WhatsApp.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1087
  </div>
1088
  </div>
1089
  </div>
1090
 
 
1091
  <div class="section">
1092
  <h2><i class="fas fa-box-open"></i> Управление товарами</h2>
1093
  <details>
 
1281
  if (group) {
1282
  const container = group.parentNode;
1283
  group.remove();
 
1284
  if (container && container.children.length === 0) {
1285
  const placeholderGroup = document.createElement('div');
1286
  placeholderGroup.className = 'color-input-group';
 
1305
  def catalog():
1306
  data = load_data()
1307
  all_products = data.get('products', [])
1308
+ categories = sorted(data.get('categories', []))
 
1309
 
 
1310
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1311
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
1312
 
 
1315
  products=products_sorted,
1316
  categories=categories,
1317
  repo_id=REPO_ID,
 
1318
  store_address=STORE_ADDRESS,
 
1319
  currency_code=CURRENCY_CODE
1320
  )
1321
 
 
1323
  def product_detail(index):
1324
  data = load_data()
1325
  all_products = data.get('products', [])
 
1326
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1327
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
1328
 
 
1329
  try:
1330
  product = products_sorted[index]
 
1331
  except IndexError:
1332
  logging.warning(f"Attempted access to non-existent or out-of-stock product with index {index}")
1333
  return "Товар не найден или отсутствует в наличии.", 404
 
1336
  PRODUCT_DETAIL_TEMPLATE,
1337
  product=product,
1338
  repo_id=REPO_ID,
 
1339
  currency_code=CURRENCY_CODE
1340
  )
1341
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1342
  @app.route('/create_order', methods=['POST'])
1343
  def create_order():
1344
  order_data = request.get_json()
 
1348
  return jsonify({"error": "Корзина пуста или не передана."}), 400
1349
 
1350
  cart_items = order_data['cart']
 
1351
 
 
1352
  total_price = 0
1353
  processed_cart = []
1354
  for item in cart_items:
 
1355
  if not all(k in item for k in ('name', 'price', 'quantity')):
1356
  logging.error(f"Invalid cart item structure received: {item}")
1357
  return jsonify({"error": "Неверный формат товара в корзине."}), 400
 
1361
  if price < 0 or quantity <= 0:
1362
  raise ValueError("Invalid price or quantity")
1363
  total_price += price * quantity
 
1364
  processed_cart.append({
1365
  "name": item['name'],
1366
  "price": price,
1367
  "quantity": quantity,
1368
  "color": item.get('color', 'N/A'),
1369
+ "photo": item.get('photo'),
 
1370
  "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/60x60.png?text=N/A"
1371
  })
1372
  except (ValueError, TypeError) as e:
 
1381
  "created_at": order_timestamp,
1382
  "cart": processed_cart,
1383
  "total_price": round(total_price, 2),
1384
+ "user_info": None, # Explicitly set to None as users are anonymous
1385
+ "status": "new"
1386
  }
1387
 
1388
  try:
1389
  data = load_data()
 
1390
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1391
  data['orders'] = {}
1392
 
1393
  data['orders'][order_id] = new_order
1394
  save_data(data)
1395
+ logging.info(f"Order {order_id} created successfully (anonymously).")
1396
+ return jsonify({"order_id": order_id}), 201
1397
 
1398
  except Exception as e:
1399
  logging.error(f"Failed to save order {order_id}: {e}", exc_info=True)
 
1401
 
1402
  @app.route('/order/<order_id>')
1403
  def view_order(order_id):
 
1404
  data = load_data()
1405
  order = data.get('orders', {}).get(order_id)
1406
 
1407
  if order:
 
 
1408
  logging.info(f"Displaying order {order_id}")
1409
  else:
1410
  logging.warning(f"Order {order_id} not found.")
 
1414
  repo_id=REPO_ID,
1415
  currency_code=CURRENCY_CODE)
1416
 
 
1417
  @app.route('/admin', methods=['GET', 'POST'])
1418
  def admin():
 
 
 
 
 
 
 
1419
  data = load_data()
 
 
1420
  products = data.get('products', [])
1421
  categories = data.get('categories', [])
 
1422
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1423
  data['orders'] = {}
1424
 
 
1431
  category_name = request.form.get('category_name', '').strip()
1432
  if category_name and category_name not in categories:
1433
  categories.append(category_name)
1434
+ data['categories'] = categories
 
1435
  save_data(data)
1436
  logging.info(f"Category '{category_name}' added.")
1437
  flash(f"Категория '{category_name}' успешно добавлена.", 'success')
 
1447
  if category_to_delete and category_to_delete in categories:
1448
  categories.remove(category_to_delete)
1449
  updated_count = 0
 
1450
  for product in products:
1451
  if product.get('category') == category_to_delete:
1452
  product['category'] = 'Без категории'
1453
  updated_count += 1
1454
+ data['categories'] = categories
1455
+ data['products'] = products
1456
  save_data(data)
1457
  logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
1458
  flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
 
1501
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1502
  continue
1503
 
1504
+ safe_name = secure_filename(name.replace(' ', '_'))[:50]
 
1505
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1506
  temp_path = os.path.join(uploads_dir, photo_filename)
1507
  photo.save(temp_path)
 
1516
  )
1517
  photos_list.append(photo_filename)
1518
  logging.info(f"Photo {photo_filename} uploaded successfully.")
1519
+ os.remove(temp_path)
1520
  uploaded_count += 1
1521
  except Exception as e:
1522
  logging.error(f"Error uploading photo {photo.filename} to HF: {e}", exc_info=True)
1523
  flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
1524
  if os.path.exists(temp_path):
1525
  try: os.remove(temp_path)
1526
+ except OSError: pass
1527
  elif photo and not photo.filename:
1528
  logging.warning("Received an empty photo file object when adding product.")
1529
  try:
 
1530
  if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
1531
  os.rmdir(uploads_dir)
1532
  except OSError as e:
 
1536
 
1537
 
1538
  new_product = {
 
1539
  'name': name, 'price': price, 'description': description,
1540
  'category': category if category in categories else 'Без категории',
1541
  'photos': photos_list, 'colors': colors,
1542
  'in_stock': in_stock, 'is_top': is_top
1543
  }
1544
  products.append(new_product)
1545
+ data['products'] = products
1546
  save_data(data)
1547
  logging.info(f"Product '{name}' added.")
1548
  flash(f"Товар '{name}' успешно добавлен.", 'success')
 
1555
 
1556
  try:
1557
  index = int(index_str)
 
1558
  if not (0 <= index < len(products)):
1559
  raise IndexError("Product index out of range")
1560
  product_to_edit = products[index]
 
1565
  logging.error(f"Invalid index '{index_str}' for editing. Product list length: {len(products)}")
1566
  return redirect(url_for('admin'))
1567
 
 
1568
  product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
1569
  price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
1570
  product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
 
1582
  logging.warning(f"Invalid price format '{price_str}' during edit of product {original_name}. Price not changed.")
1583
  flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
1584
 
 
1585
  photos_files = request.files.getlist('photos')
1586
  if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE:
1587
  uploads_dir = 'uploads_temp'
 
1630
 
1631
  if new_photos_list:
1632
  logging.info(f"New photo list for product {product_to_edit['name']} generated.")
 
1633
  old_photos = product_to_edit.get('photos', [])
1634
  if old_photos:
1635
  logging.info(f"Attempting to delete old photos: {old_photos}")
 
1643
  )
1644
  logging.info(f"Old photos for product {product_to_edit['name']} deleted from HF.")
1645
  except Exception as e:
 
1646
  logging.error(f"Error deleting old photos {old_photos} from HF: {e}", exc_info=True)
1647
  flash("Не удалось удалить старые фотографии с сервера. Новые фото загружены.", "warning")
 
1648
  product_to_edit['photos'] = new_photos_list
1649
  flash("Фотографии товара успешно обновлены.", "success")
1650
  elif uploaded_count == 0 and any(f.filename for f in photos_files):
 
1651
  flash("Не удалось загрузить новые фотографии (возможно, неверный формат).", "error")
1652
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
1653
  flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning")
1654
 
 
1655
  products[index] = product_to_edit
1656
+ data['products'] = products
1657
  save_data(data)
1658
  logging.info(f"Product '{original_name}' (index {index}) updated to '{product_to_edit['name']}'.")
1659
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
 
1665
  return redirect(url_for('admin'))
1666
  try:
1667
  index = int(index_str)
 
1668
  if not (0 <= index < len(products)): raise IndexError("Product index out of range")
1669
  deleted_product = products.pop(index)
1670
  product_name = deleted_product.get('name', 'N/A')
1671
 
 
1672
  photos_to_delete = deleted_product.get('photos', [])
1673
  if photos_to_delete and HF_TOKEN_WRITE:
1674
  logging.info(f"Attempting to delete photos for product '{product_name}' from HF: {photos_to_delete}")
 
1683
  )
1684
  logging.info(f"Photos for product '{product_name}' deleted from HF.")
1685
  except Exception as e:
 
1686
  logging.error(f"Error deleting photos {photos_to_delete} for product '{product_name}' from HF: {e}", exc_info=True)
1687
  flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning")
1688
  elif photos_to_delete and not HF_TOKEN_WRITE:
 
1690
  flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
1691
 
1692
 
1693
+ data['products'] = products
1694
  save_data(data)
1695
  logging.info(f"Product '{product_name}' (original index {index}) deleted.")
1696
  flash(f"Товар '{product_name}' удален.", 'success')
 
1698
  flash(f"Ошибка удаления: неверный индекс товара '{index_str}'.", 'error')
1699
  logging.error(f"Invalid index '{index_str}' for deletion. Product list length: {len(products)}")
1700
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1701
  else:
1702
  logging.warning(f"Received unknown admin action: {action}")
1703
  flash(f"Неизвестное действие: {action}", 'warning')
1704
 
 
1705
  return redirect(url_for('admin'))
1706
 
1707
  except Exception as e:
 
1708
  logging.error(f"Error processing admin action '{action}': {e}", exc_info=True)
1709
  flash(f"Произошла внутренняя ошибка при выполнении дейс��вия '{action}'. Подробности в логе сервера.", 'error')
1710
  return redirect(url_for('admin'))
1711
 
1712
  # --- GET request ---
 
1713
  current_data = load_data()
 
 
1714
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
1715
  display_categories = sorted(current_data.get('categories', []))
 
1716
 
1717
  return render_template_string(
1718
  ADMIN_TEMPLATE,
1719
  products=display_products,
1720
  categories=display_categories,
 
1721
  repo_id=REPO_ID,
1722
  currency_code=CURRENCY_CODE
1723
  )
1724
 
1725
  @app.route('/force_upload', methods=['POST'])
1726
  def force_upload():
 
1727
  logging.info("Forcing upload to Hugging Face...")
1728
  try:
1729
+ upload_db_to_hf()
1730
  flash("Данные успешно загружены на Hugging Face.", 'success')
1731
  except Exception as e:
1732
  logging.error(f"Error during forced upload: {e}", exc_info=True)
 
1735
 
1736
  @app.route('/force_download', methods=['POST'])
1737
  def force_download():
 
1738
  logging.info("Forcing download from Hugging Face...")
1739
  try:
1740
+ if download_db_from_hf():
1741
  flash("Данные успешно скачаны с Hugging Face. Локальные файлы обновлены.", 'success')
1742
+ load_data() # Reload data in memory after download
 
 
1743
  else:
1744
  flash("Не удалось скачать данные с Hugging Face после нескольких попыток. Проверьте логи.", 'error')
1745
  except Exception as e:
 
1751
  # --- App Initialization ---
1752
 
1753
  if __name__ == '__main__':
 
1754
  logging.info("Application starting up. Performing initial data load/download...")
1755
+ download_db_from_hf()
1756
+ load_data()
 
1757
  logging.info("Initial data load complete.")
1758
 
 
1759
  if HF_TOKEN_WRITE:
1760
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1761
  backup_thread.start()
 
1765
 
1766
  port = int(os.environ.get('PORT', 7860))
1767
  logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
 
 
 
1768
  app.run(debug=False, host='0.0.0.0', port=port)
1769
 
1770
  # --- END OF FILE app.py ---