Kgshop commited on
Commit
5b5714b
·
verified ·
1 Parent(s): cd500e4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +125 -306
app.py CHANGED
@@ -1,6 +1,4 @@
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
@@ -14,42 +12,28 @@ 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'
22
  DATA_FILE = 'data.json'
23
-
24
-
25
  SYNC_FILES = [DATA_FILE]
26
-
27
  REPO_ID = "Kgshop/nizhbel"
28
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
29
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
30
-
31
  STORE_ADDRESS = "Рынок Кербент, 6 ряд , 3 контейнер / 5 ряд 25 контейнер "
32
-
33
  CURRENCY_CODE = 'KGS'
34
  CURRENCY_NAME = 'Кыргызский сом'
35
-
36
  DOWNLOAD_RETRIES = 3
37
- DOWNLOAD_DELAY = 5 # seconds
38
-
39
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
40
-
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
-
49
  files_to_download = [specific_file] if specific_file else SYNC_FILES
50
  logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
51
  all_successful = True
52
-
53
  for file_name in files_to_download:
54
  success = False
55
  for attempt in range(retries + 1):
@@ -90,17 +74,13 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
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)
93
-
94
  if attempt < retries:
95
  time.sleep(delay)
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
103
-
104
  def upload_db_to_hf(specific_file=None):
105
  if not HF_TOKEN_WRITE:
106
  logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.")
@@ -109,7 +89,6 @@ def upload_db_to_hf(specific_file=None):
109
  api = HfApi()
110
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
111
  logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
112
-
113
  for file_name in files_to_upload:
114
  if os.path.exists(file_name):
115
  try:
@@ -129,7 +108,6 @@ def upload_db_to_hf(specific_file=None):
129
  logging.info("Finished uploading files to HF.")
130
  except Exception as e:
131
  logging.error(f"General error during Hugging Face upload initialization or process: {e}", exc_info=True)
132
-
133
  def periodic_backup():
134
  backup_interval = 1800
135
  logging.info(f"Setting up periodic backup every {backup_interval} seconds.")
@@ -138,9 +116,6 @@ def periodic_backup():
138
  logging.info("Starting periodic backup...")
139
  upload_db_to_hf()
140
  logging.info("Periodic backup finished.")
141
-
142
- # --- Data Loading and Saving Functions ---
143
-
144
  def load_data():
145
  default_data = {'products': [], 'categories': [], 'orders': {}}
146
  try:
@@ -158,7 +133,6 @@ def load_data():
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:
@@ -190,7 +164,6 @@ def load_data():
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):
@@ -199,112 +172,107 @@ def save_data(data):
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)
205
  logging.info(f"Data successfully saved to {DATA_FILE}")
206
  upload_db_to_hf(specific_file=DATA_FILE)
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 = '''
213
  <!DOCTYPE html>
214
  <html lang="ru">
215
  <head>
216
  <meta charset="UTF-8">
217
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
218
- <title>Aikas_optom - Каталог</title>
219
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
220
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
221
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
222
  <style>
223
  * { margin: 0; padding: 0; box-sizing: border-box; }
224
- body { font-family: 'Poppins', sans-serif; background: #FFF0F5; color: #4d333f; line-height: 1.6; transition: background 0.3s, color 0.3s; }
225
- body.dark-mode { background: #2c1a2b; color: #fce7f3; }
226
  .container { max-width: 1300px; margin: 0 auto; padding: 20px; }
227
- .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #FBCFE8; }
228
- body.dark-mode .header { border-bottom-color: #59344f; }
229
- .header h1 { font-size: 1.8rem; font-weight: 600; color: #F472B6; }
230
- .theme-toggle { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #d1a7b8; transition: color 0.3s ease; }
231
- .theme-toggle:hover { color: #DB2777; }
232
- body.dark-mode .theme-toggle { color: #fbcfe8; }
233
- body.dark-mode .theme-toggle:hover { color: #f9a8d4; }
234
- .store-address { padding: 15px; text-align: center; background-color: #ffffff; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); font-size: 1rem; color: #6b4d5d; }
235
- body.dark-mode .store-address { background-color: #412539; color: #fbcfe8; }
236
  .filters-container { margin: 20px 0; display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
237
  .search-container { margin: 20px 0; text-align: center; }
238
- #search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #FBCFE8; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: all 0.3s ease; }
239
- body.dark-mode #search-input { background-color: #412539; border-color: #59344f; color: #fce7f3; }
240
- #search-input:focus { border-color: #F472B6; box-shadow: 0 0 0 3px rgba(244, 114, 182, 0.2); }
241
- body.dark-mode #search-input:focus { border-color: #EC4899; box-shadow: 0 0 0 3px rgba(236, 72, 153, 0.3); }
242
- .category-filter { padding: 8px 16px; border: 1px solid #FBCFE8; border-radius: 20px; background-color: #fff; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.9rem; font-weight: 400; color: #DB2777; }
243
- body.dark-mode .category-filter { background-color: #412539; border-color: #59344f; color: #f9a8d4; }
244
- .category-filter.active, .category-filter:hover { background-color: #F472B6; color: white; border-color: #F472B6; box-shadow: 0 2px 10px rgba(244, 114, 182, 0.3); }
245
- body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { background-color: #EC4899; border-color: #EC4899; color: #2c1a2b; box-shadow: 0 2px 10px rgba(236, 72, 153, 0.4); }
246
  .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 20px; padding: 10px; }
247
  @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
248
  @media (min-width: 900px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } }
249
-
250
- .product { background: #fff; border-radius: 15px; padding: 0; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; height: 100%; border: 1px solid #FCE7F3;}
251
- body.dark-mode .product { background: #412539; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); border-color: #59344f; }
252
  .product:hover { transform: translateY(-5px) scale(1.02); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); }
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: #4d333f; }
258
- body.dark-mode .product h2 { color: #fce7f3; }
259
- .product-price { font-size: 1.2rem; color: #DB2777; font-weight: 700; text-align: center; margin: 5px 0; }
260
- body.dark-mode .product-price { color: #f9a8d4; }
261
- .product-description { font-size: 0.85rem; color: #b88fa7; text-align: center; margin-bottom: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
262
- body.dark-mode .product-description { color: #fbcfe8; }
263
  .product-actions { padding: 0 15px 15px 15px; display: flex; flex-direction: column; gap: 8px; }
264
- .product-button { display: block; width: 100%; padding: 10px; border: none; border-radius: 8px; background-color: #F472B6; color: white; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); text-align: center; text-decoration: none; }
265
- .product-button:hover { background-color: #DB2777; box-shadow: 0 4px 15px rgba(219, 39, 119, 0.4); transform: translateY(-2px); }
266
  .product-button i { margin-right: 5px; }
267
- .add-to-cart { background-color: #F472B6; } /* Changed from green */
268
- .add-to-cart:hover { background-color: #DB2777; box-shadow: 0 4px 15px rgba(219, 39, 119, 0.4); } /* Changed from green */
269
- #cart-button { position: fixed; bottom: 25px; right: 25px; background-color: #F472B6; color: white; border: none; border-radius: 50%; width: 55px; height: 55px; font-size: 1.5rem; cursor: pointer; display: none; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(244, 114, 182, 0.4); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1000; }
270
  #cart-button .fa-shopping-cart { margin-right: 0; }
271
- #cart-button span { position: absolute; top: -5px; right: -5px; background-color: #DB2777; color: white; border-radius: 50%; padding: 2px 6px; font-size: 0.7rem; font-weight: bold; }
272
  .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(5px); overflow-y: auto; }
273
  .modal-content { background: #ffffff; margin: 5% auto; padding: 25px; border-radius: 15px; width: 90%; max-width: 700px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); animation: slideIn 0.3s ease-out; position: relative; }
274
- body.dark-mode .modal-content { background: #412539; color: #fce7f3; }
275
  @keyframes slideIn { from { transform: translateY(-30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
276
  .close { position: absolute; top: 15px; right: 15px; font-size: 1.8rem; color: #aaa; cursor: pointer; transition: color 0.3s; line-height: 1; }
277
  .close:hover { color: #333; }
278
- body.dark-mode .close { color: #fbcfe8; }
279
  body.dark-mode .close:hover { color: #ffffff; }
280
- .modal-content h2 { margin-top: 0; margin-bottom: 20px; color: #F472B6; display: flex; align-items: center; gap: 10px;}
281
- body.dark-mode .modal-content h2 { color: #f9a8d4; }
282
- .cart-item { display: grid; grid-template-columns: auto 1fr auto auto; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid #FBCFE8; }
283
- body.dark-mode .cart-item { border-bottom-color: #59344f; }
284
  .cart-item:last-child { border-bottom: none; }
285
  .cart-item img { width: 60px; height: 60px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; grid-column: 1; }
286
  .cart-item-details { grid-column: 2; }
287
  .cart-item-details strong { display: block; margin-bottom: 5px; font-size: 1rem; }
288
- .cart-item-price { font-size: 0.9rem; color: #6b4d5d; }
289
- body.dark-mode .cart-item-price { color: #fbcfe8; }
290
  .cart-item-total { font-weight: bold; text-align: right; grid-column: 3; font-size: 1rem;}
291
  .cart-item-remove { grid-column: 4; background:none; border:none; color:#f56565; cursor:pointer; font-size: 1.3em; padding: 5px; line-height: 1; }
292
  .cart-item-remove:hover { color: #c53030; }
293
- .quantity-input, .color-select { width: 100%; max-width: 180px; padding: 10px; border: 1px solid #FBCFE8; border-radius: 8px; font-size: 1rem; margin: 10px 0; box-sizing: border-box; }
294
- body.dark-mode .quantity-input, body.dark-mode .color-select { background-color: #2c1a2b; border-color: #59344f; color: #fce7f3; }
295
- .cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #FBCFE8; padding-top: 15px; }
296
- body.dark-mode .cart-summary { border-top-color: #59344f; }
297
  .cart-summary strong { font-size: 1.2rem; }
298
  .cart-actions { margin-top: 25px; display: flex; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
299
  .cart-actions .product-button { width: auto; flex-grow: 1; }
300
- .clear-cart { background-color: #a0aec0; } /* Grey color */
301
  .clear-cart:hover { background-color: #718096; box-shadow: 0 4px 15px rgba(113, 128, 150, 0.4); }
302
- .formulate-order-button { background-color: #F472B6; } /* Pink color */
303
- .formulate-order-button:hover { background-color: #DB2777; box-shadow: 0 4px 15px rgba(219, 39, 119, 0.4); } /* Darker Pink */
304
- .notification { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background-color: #F472B6; color: white; padding: 10px 20px; border-radius: 20px; box-shadow: 0 4px 10px rgba(0,0,0,0.2); z-index: 1002; opacity: 0; transition: opacity 0.5s ease; font-size: 0.9rem;}
305
  .notification.show { opacity: 1;}
306
  .no-results-message { grid-column: 1 / -1; text-align: center; padding: 40px; font-size: 1.1rem; color: #a0aec0; }
307
- body.dark-mode .no-results-message { color: #fbcfe8; }
308
  .top-product-indicator { position: absolute; top: 8px; right: 8px; background-color: rgba(255, 215, 0, 0.8); color: #333; padding: 2px 6px; font-size: 0.7rem; border-radius: 4px; font-weight: bold; z-index: 10; backdrop-filter: blur(2px); }
309
  .product { position: relative; }
310
  </style>
@@ -313,27 +281,23 @@ CATALOG_TEMPLATE = '''
313
  <div class="container">
314
  <div class="header">
315
  <div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
316
- <img src="https://cdn-avatars.huggingface.co/v1/production/uploads/67c280ccb9d3dfdee58ecfdd/p2TVtSc6UQr0De1jDYBLk.jpeg" alt="Aikas_optom Logo" style="height: 40px; width: auto; border-radius: 4px;">
317
- <h1>Aikas_optom</h1>
318
  </div>
319
  <button class="theme-toggle" onclick="toggleTheme()" aria-label="Переключить тему">
320
  <i class="fas fa-moon"></i>
321
  </button>
322
  </div>
323
-
324
  <div class="store-address">Наш адрес: {{ store_address }}</div>
325
-
326
  <div class="filters-container">
327
  <button class="category-filter active" data-category="all">Все категории</button>
328
  {% for category in categories %}
329
  <button class="category-filter" data-category="{{ category }}">{{ category }}</button>
330
  {% endfor %}
331
  </div>
332
-
333
  <div class="search-container">
334
  <input type="text" id="search-input" placeholder="Поиск по названию или описанию...">
335
  </div>
336
-
337
  <div class="products-grid" id="products-grid">
338
  {% for product in products %}
339
  <div class="product"
@@ -370,14 +334,12 @@ CATALOG_TEMPLATE = '''
370
  {% endif %}
371
  </div>
372
  </div>
373
-
374
  <div id="productModal" class="modal">
375
  <div class="modal-content">
376
  <span class="close" onclick="closeModal('productModal')" aria-label="Закрыть">×</span>
377
  <div id="modalContent">Загрузка...</div>
378
  </div>
379
  </div>
380
-
381
  <div id="quantityModal" class="modal">
382
  <div class="modal-content">
383
  <span class="close" onclick="closeModal('quantityModal')" aria-label="Закрыть">×</span>
@@ -389,7 +351,6 @@ CATALOG_TEMPLATE = '''
389
  <button class="product-button add-to-cart" onclick="confirmAddToCart()"><i class="fas fa-check"></i> Добавить в корзину</button>
390
  </div>
391
  </div>
392
-
393
  <div id="cartModal" class="modal">
394
  <div class="modal-content">
395
  <span class="close" onclick="closeModal('cartModal')" aria-label="Закрыть">×</span>
@@ -408,14 +369,11 @@ CATALOG_TEMPLATE = '''
408
  </div>
409
  </div>
410
  </div>
411
-
412
  <button id="cart-button" onclick="openCartModal()" aria-label="Открыть корзину">
413
  <i class="fas fa-shopping-cart"></i>
414
  <span id="cart-count">0</span>
415
  </button>
416
-
417
  <div id="notification-placeholder"></div>
418
-
419
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
420
  <script>
421
  const products = {{ products|tojson }};
@@ -423,7 +381,6 @@ CATALOG_TEMPLATE = '''
423
  const currencyCode = '{{ currency_code }}';
424
  let selectedProductIndex = null;
425
  let cart = JSON.parse(localStorage.getItem('soolaCart') || '[]');
426
-
427
  function toggleTheme() {
428
  document.body.classList.toggle('dark-mode');
429
  const icon = document.querySelector('.theme-toggle i');
@@ -432,7 +389,6 @@ CATALOG_TEMPLATE = '''
432
  icon.classList.toggle('fa-sun', isDarkMode);
433
  localStorage.setItem('soolaTheme', isDarkMode ? 'dark' : 'light');
434
  }
435
-
436
  function applyInitialTheme() {
437
  const savedTheme = localStorage.getItem('soolaTheme');
438
  if (savedTheme === 'dark') {
@@ -441,7 +397,6 @@ CATALOG_TEMPLATE = '''
441
  if (icon) icon.classList.replace('fa-moon', 'fa-sun');
442
  }
443
  }
444
-
445
  function openModal(index) {
446
  loadProductDetails(index);
447
  const modal = document.getElementById('productModal');
@@ -450,7 +405,6 @@ CATALOG_TEMPLATE = '''
450
  document.body.style.overflow = 'hidden';
451
  }
452
  }
453
-
454
  function closeModal(modalId) {
455
  const modal = document.getElementById(modalId);
456
  if (modal) {
@@ -461,7 +415,6 @@ CATALOG_TEMPLATE = '''
461
  document.body.style.overflow = 'auto';
462
  }
463
  }
464
-
465
  function loadProductDetails(index) {
466
  const modalContent = document.getElementById('modalContent');
467
  if (!modalContent) return;
@@ -480,7 +433,6 @@ CATALOG_TEMPLATE = '''
480
  modalContent.innerHTML = `<p style="color: red; text-align:center; padding: 40px;">Не удалось загрузить информацию о товаре. ${error.message}</p>`;
481
  });
482
  }
483
-
484
  function initializeSwiper() {
485
  const swiperContainer = document.querySelector('#productModal .swiper-container');
486
  if (swiperContainer) {
@@ -496,7 +448,6 @@ CATALOG_TEMPLATE = '''
496
  });
497
  }
498
  }
499
-
500
  function openQuantityModal(index) {
501
  selectedProductIndex = index;
502
  const product = products[index];
@@ -505,13 +456,10 @@ CATALOG_TEMPLATE = '''
505
  alert("Ошибка: товар не найден.");
506
  return;
507
  }
508
-
509
  const colorSelect = document.getElementById('colorSelect');
510
  const colorLabel = document.querySelector('label[for="colorSelect"]');
511
  colorSelect.innerHTML = '';
512
-
513
  const validColors = product.colors ? product.colors.filter(c => c && c.trim() !== "") : [];
514
-
515
  if (validColors.length > 0) {
516
  validColors.forEach(color => {
517
  const option = document.createElement('option');
@@ -525,7 +473,6 @@ CATALOG_TEMPLATE = '''
525
  colorSelect.style.display = 'none';
526
  if(colorLabel) colorLabel.style.display = 'none';
527
  }
528
-
529
  document.getElementById('quantityInput').value = 1;
530
  const modal = document.getElementById('quantityModal');
531
  if(modal) {
@@ -533,30 +480,24 @@ CATALOG_TEMPLATE = '''
533
  document.body.style.overflow = 'hidden';
534
  }
535
  }
536
-
537
  function confirmAddToCart() {
538
  if (selectedProductIndex === null) return;
539
-
540
  const quantityInput = document.getElementById('quantityInput');
541
  const quantity = parseInt(quantityInput.value);
542
  const colorSelect = document.getElementById('colorSelect');
543
  const color = colorSelect.style.display !== 'none' && colorSelect.value ? colorSelect.value : 'N/A';
544
-
545
  if (isNaN(quantity) || quantity <= 0) {
546
  alert("Пожалуйста, укажите корректное количество (больше 0).");
547
  quantityInput.focus();
548
  return;
549
  }
550
-
551
  const product = products[selectedProductIndex];
552
  if (!product) {
553
  alert("Ошибка добавления: товар не найден.");
554
  return;
555
  }
556
-
557
- const cartItemId = `${product.name}-${color}`; // Use name + color as ID
558
  const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
559
-
560
  if (existingItemIndex > -1) {
561
  cart[existingItemIndex].quantity += quantity;
562
  } else {
@@ -569,21 +510,17 @@ CATALOG_TEMPLATE = '''
569
  color: color
570
  });
571
  }
572
-
573
  localStorage.setItem('soolaCart', JSON.stringify(cart));
574
  closeModal('quantityModal');
575
  updateCartButton();
576
  showNotification(`${product.name} добавлен в корзину!`);
577
  }
578
-
579
  function updateCartButton() {
580
  const cartCountElement = document.getElementById('cart-count');
581
  const cartButton = document.getElementById('cart-button');
582
  if (!cartCountElement || !cartButton) return;
583
-
584
  let totalItems = 0;
585
  cart.forEach(item => { totalItems += item.quantity; });
586
-
587
  if (totalItems > 0) {
588
  cartCountElement.textContent = totalItems;
589
  cartButton.style.display = 'flex';
@@ -592,14 +529,11 @@ CATALOG_TEMPLATE = '''
592
  cartButton.style.display = 'none';
593
  }
594
  }
595
-
596
  function openCartModal() {
597
  const cartContent = document.getElementById('cartContent');
598
  const cartTotalElement = document.getElementById('cartTotal');
599
  if (!cartContent || !cartTotalElement) return;
600
-
601
  let total = 0;
602
-
603
  if (cart.length === 0) {
604
  cartContent.innerHTML = '<p style="text-align: center; padding: 20px;">Ваша корзина пуста.</p>';
605
  cartTotalElement.textContent = '0.00';
@@ -611,7 +545,6 @@ CATALOG_TEMPLATE = '''
611
  ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
612
  : 'https://via.placeholder.com/60x60.png?text=N/A';
613
  const colorText = item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
614
-
615
  return `
616
  <div class="cart-item">
617
  <img src="${photoUrl}" alt="${item.name}">
@@ -632,14 +565,12 @@ CATALOG_TEMPLATE = '''
632
  document.body.style.overflow = 'hidden';
633
  }
634
  }
635
-
636
  function removeFromCart(itemId) {
637
  cart = cart.filter(item => item.id !== itemId);
638
  localStorage.setItem('soolaCart', JSON.stringify(cart));
639
  openCartModal();
640
  updateCartButton();
641
  }
642
-
643
  function clearCart() {
644
  if (confirm("Вы уверены, что хотите очистить корзину?")) {
645
  cart = [];
@@ -648,22 +579,17 @@ CATALOG_TEMPLATE = '''
648
  updateCartButton();
649
  }
650
  }
651
-
652
  function formulateOrder() {
653
  if (cart.length === 0) {
654
  alert("Корзина пуста! Добавьте товары перед формированием заказа.");
655
  return;
656
  }
657
-
658
  const orderData = {
659
  cart: cart
660
  };
661
-
662
  const formulateButton = document.querySelector('.formulate-order-button');
663
  if (formulateButton) formulateButton.disabled = true;
664
-
665
  showNotification("Формируем заказ...", 5000);
666
-
667
  fetch('/create_order', {
668
  method: 'POST',
669
  headers: { 'Content-Type': 'application/json' },
@@ -692,26 +618,20 @@ CATALOG_TEMPLATE = '''
692
  if (formulateButton) formulateButton.disabled = false;
693
  });
694
  }
695
-
696
-
697
  function filterProducts() {
698
  const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
699
  const activeCategoryButton = document.querySelector('.category-filter.active');
700
  const activeCategory = activeCategoryButton ? activeCategoryButton.dataset.category : 'all';
701
  const grid = document.getElementById('products-grid');
702
  let visibleProducts = 0;
703
-
704
  const existingNoResults = grid.querySelector('.no-results-message');
705
  if (existingNoResults) existingNoResults.remove();
706
-
707
  document.querySelectorAll('.products-grid .product').forEach(productElement => {
708
  const name = productElement.getAttribute('data-name');
709
  const description = productElement.getAttribute('data-description');
710
  const category = productElement.getAttribute('data-category');
711
-
712
  const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
713
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
714
-
715
  if (matchesSearch && matchesCategory) {
716
  productElement.style.display = 'flex';
717
  visibleProducts++;
@@ -719,7 +639,6 @@ CATALOG_TEMPLATE = '''
719
  productElement.style.display = 'none';
720
  }
721
  });
722
-
723
  if (visibleProducts === 0 && products.length > 0) {
724
  const p = document.createElement('p');
725
  p.className = 'no-results-message';
@@ -732,13 +651,10 @@ CATALOG_TEMPLATE = '''
732
  grid.appendChild(p);
733
  }
734
  }
735
-
736
  function setupFilters() {
737
  const searchInput = document.getElementById('search-input');
738
  const categoryFilters = document.querySelectorAll('.category-filter');
739
-
740
  if(searchInput) searchInput.addEventListener('input', filterProducts);
741
-
742
  categoryFilters.forEach(filter => {
743
  filter.addEventListener('click', function() {
744
  categoryFilters.forEach(f => f.classList.remove('active'));
@@ -748,7 +664,6 @@ CATALOG_TEMPLATE = '''
748
  });
749
  filterProducts();
750
  }
751
-
752
  function showNotification(message, duration = 3000) {
753
  const placeholder = document.getElementById('notification-placeholder');
754
  if (!placeholder) {
@@ -762,34 +677,26 @@ CATALOG_TEMPLATE = '''
762
  document.body.appendChild(newPlaceholder);
763
  placeholder = newPlaceholder;
764
  }
765
-
766
-
767
  const notification = document.createElement('div');
768
  notification.className = 'notification';
769
  notification.textContent = message;
770
  placeholder.appendChild(notification);
771
-
772
  void notification.offsetWidth;
773
-
774
  notification.classList.add('show');
775
-
776
  setTimeout(() => {
777
  notification.classList.remove('show');
778
  notification.addEventListener('transitionend', () => notification.remove());
779
  }, duration);
780
  }
781
-
782
  document.addEventListener('DOMContentLoaded', () => {
783
  applyInitialTheme();
784
  updateCartButton();
785
  setupFilters();
786
-
787
  window.addEventListener('click', function(event) {
788
  if (event.target.classList.contains('modal')) {
789
  closeModal(event.target.id);
790
  }
791
  });
792
-
793
  window.addEventListener('keydown', function(event) {
794
  if (event.key === 'Escape') {
795
  document.querySelectorAll('.modal[style*="display: block"]').forEach(modal => {
@@ -798,15 +705,13 @@ CATALOG_TEMPLATE = '''
798
  }
799
  });
800
  });
801
-
802
  </script>
803
  </body>
804
  </html>
805
  '''
806
-
807
  PRODUCT_DETAIL_TEMPLATE = '''
808
  <div style="padding: 10px;">
809
- <h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 15px; text-align: center; color: #F472B6;">{{ product['name'] }}</h2>
810
  <div class="swiper-container" style="max-width: 450px; margin: 0 auto 20px; border-radius: 10px; overflow: hidden; background-color: #fff;">
811
  <div class="swiper-wrapper">
812
  {% if product.get('photos') and product['photos']|length > 0 %}
@@ -827,14 +732,13 @@ PRODUCT_DETAIL_TEMPLATE = '''
827
  </div>
828
  {% if product.get('photos') and product['photos']|length > 1 %}
829
  <div class="swiper-pagination" style="position: relative; bottom: 5px;"></div>
830
- <div class="swiper-button-next" style="color: #F472B6;"></div>
831
- <div class="swiper-button-prev" style="color: #F472B6;"></div>
832
  {% endif %}
833
  </div>
834
-
835
  <div style="margin-top: 20px; font-size: 1rem; line-height: 1.7;">
836
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
837
- <p style="font-size: 1.2rem; font-weight: bold; color: #DB2777;"><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
838
  <p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\\n', '<br>')|safe }}</p>
839
  {% set colors = product.get('colors', []) %}
840
  {% if colors and colors|select('ne', '')|list|length > 0 %}
@@ -843,40 +747,39 @@ PRODUCT_DETAIL_TEMPLATE = '''
843
  </div>
844
  </div>
845
  '''
846
-
847
  ORDER_TEMPLATE = '''
848
  <!DOCTYPE html>
849
  <html lang="ru">
850
  <head>
851
  <meta charset="UTF-8">
852
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
853
- <title>Заказ №{{ order.id }} - Aikas_optom</title>
854
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
855
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
856
  <style>
857
- body { font-family: 'Poppins', sans-serif; background: #FFF0F5; color: #4d333f; line-height: 1.6; padding: 20px; }
858
- .container { max-width: 800px; margin: 20px auto; padding: 30px; background: #fff; border-radius: 15px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); border: 1px solid #FBCFE8; }
859
- h1 { text-align: center; color: #F472B6; margin-bottom: 25px; font-size: 1.8rem; font-weight: 600; }
860
- h2 { color: #DB2777; margin-top: 30px; margin-bottom: 15px; font-size: 1.4rem; border-bottom: 1px solid #FBCFE8; padding-bottom: 8px;}
861
  .order-meta { font-size: 0.9rem; color: #a0aec0; margin-bottom: 20px; text-align: center; }
862
- .order-item { display: grid; grid-template-columns: 60px 1fr auto; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid #FCE7F3; }
863
  .order-item:last-child { border-bottom: none; }
864
- .order-item img { width: 60px; height: 60px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; border: 1px solid #FCE7F3;}
865
- .item-details strong { display: block; margin-bottom: 5px; font-size: 1.05rem; color: #4d333f;}
866
- .item-details span { font-size: 0.9rem; color: #6b4d5d; display: block;}
867
- .item-total { font-weight: bold; text-align: right; font-size: 1rem; color: #DB2777;}
868
- .order-summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid #F472B6; text-align: right; }
869
  .order-summary p { margin-bottom: 10px; font-size: 1.1rem; }
870
- .order-summary strong { font-size: 1.3rem; color: #F472B6; }
871
- .customer-info { margin-top: 30px; background-color: #fff7fa; padding: 20px; border-radius: 8px; border: 1px solid #FCE7F3;}
872
  .customer-info p { margin-bottom: 8px; font-size: 0.95rem; }
873
- .customer-info strong { color: #DB2777; }
874
  .actions { margin-top: 30px; text-align: center; }
875
- .button { padding: 12px 25px; border: none; border-radius: 8px; background-color: #F472B6; 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; }
876
- .button:hover { background-color: #DB2777; }
877
  .button:active { transform: scale(0.98); }
878
  .button i { font-size: 1.2rem; }
879
- .catalog-link { display: block; text-align: center; margin-top: 25px; color: #EC4899; text-decoration: none; font-size: 0.9rem; }
880
  .catalog-link:hover { text-decoration: underline; }
881
  .not-found { text-align: center; color: #c53030; font-size: 1.2rem; padding: 40px 0;}
882
  </style>
@@ -886,7 +789,6 @@ ORDER_TEMPLATE = '''
886
  {% if order %}
887
  <h1><i class="fas fa-receipt"></i> Ваш Заказ №{{ order.id }}</h1>
888
  <p class="order-meta">Дата создания: {{ order.created_at }}</p>
889
-
890
  <h2><i class="fas fa-shopping-bag"></i> Товары в заказе</h2>
891
  <div id="orderItems">
892
  {% for item in order.cart %}
@@ -902,40 +804,32 @@ ORDER_TEMPLATE = '''
902
  </div>
903
  {% endfor %}
904
  </div>
905
-
906
  <div class="order-summary">
907
  <p>Общая сумма товаров: <strong>{{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
908
  <p><strong>ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
909
  </div>
910
-
911
  <div class="customer-info">
912
  <h2><i class="fas fa-info-circle"></i> Статус заказа</h2>
913
  <p>Этот заказ был оформлен без входа в систему.</p>
914
  <p>Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.</p>
915
  </div>
916
-
917
  <div class="actions">
918
  <button class="button" onclick="sendOrderViaWhatsApp()"><i class="fab fa-whatsapp"></i> Отправить заказ</button>
919
  </div>
920
-
921
  <a href="{{ url_for('catalog') }}" class="catalog-link">← Вернуться в каталог</a>
922
-
923
  <script>
924
  function sendOrderViaWhatsApp() {
925
  const orderId = '{{ order.id }}';
926
  const orderUrl = `{{ request.url }}`;
927
- const whatsappNumber = "996507003777"; /* UPDATED NUMBER */
928
-
929
- let message = `Здравствуйте! Хочу подтвердить свой заказ на Aikas_optom:%0A%0A`; /* UPDATED NAME */
930
  message += `*Номер заказа:* ${orderId}%0A`;
931
  message += `*Ссылка на заказ:* ${encodeURIComponent(orderUrl)}%0A%0A`;
932
  message += `Пожалуйста, свяжитесь со мной для уточнения деталей оплаты и доставки.`;
933
-
934
  const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${message}`;
935
  window.open(whatsappUrl, '_blank');
936
  }
937
  </script>
938
-
939
  {% else %}
940
  <h1 style="color: #c53030;"><i class="fas fa-exclamation-triangle"></i> Ошибка</h1>
941
  <p class="not-found">Заказ с таким ID не найден.</p>
@@ -945,90 +839,87 @@ ORDER_TEMPLATE = '''
945
  </body>
946
  </html>
947
  '''
948
-
949
  ADMIN_TEMPLATE = '''
950
  <!DOCTYPE html>
951
  <html lang="ru">
952
  <head>
953
  <meta charset="UTF-8">
954
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
955
- <title>Админ-панель - Aikas_optom</title>
956
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
957
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
958
  <style>
959
- body { font-family: 'Poppins', sans-serif; background-color: #fff7fa; color: #4d333f; padding: 20px; line-height: 1.6; }
960
  .container { max-width: 1200px; margin: 0 auto; background-color: #fff; padding: 25px; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.05); }
961
- .header { padding-bottom: 15px; margin-bottom: 25px; border-bottom: 1px solid #FBCFE8; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;}
962
- h1, h2, h3 { font-weight: 600; color: #F472B6; margin-bottom: 15px; }
963
  h1 { font-size: 1.8rem; }
964
  h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
965
- h3 { font-size: 1.2rem; color: #DB2777; margin-top: 20px; }
966
- .section { margin-bottom: 30px; padding: 20px; background-color: #fff7fa; border: 1px solid #FBCFE8; border-radius: 8px; }
967
  form { margin-bottom: 20px; }
968
- label { font-weight: 500; margin-top: 10px; display: block; color: #6b4d5d; font-size: 0.9rem;}
969
- input[type="text"], input[type="number"], input[type="password"], input[type="tel"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #fbcfe8; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; background-color: #fff; }
970
- input:focus, textarea:focus, select:focus { border-color: #F472B6; outline: none; box-shadow: 0 0 0 2px rgba(244, 114, 182, 0.1); }
971
  textarea { min-height: 80px; resize: vertical; }
972
- input[type="file"] { padding: 8px; background-color: #fff0f5; cursor: pointer; border: 1px solid #fbcfe8;}
973
- input[type="file"]::file-selector-button { padding: 5px 10px; border-radius: 4px; background-color: #fce7f3; border: 1px solid #fbcfe8; cursor: pointer; margin-right: 10px;}
974
  input[type="checkbox"] { margin-right: 5px; vertical-align: middle; }
975
  label.inline-label { display: inline-block; margin-top: 10px; font-weight: normal; }
976
- button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #F472B6; color: white; font-weight: 500; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; margin-top: 15px; font-size: 0.95rem; display: inline-flex; align-items: center; gap: 5px; text-decoration: none; line-height: 1.2;}
977
- button:hover, .button:hover { background-color: #DB2777; }
978
  button:active, .button:active { transform: scale(0.98); }
979
  button[type="submit"] { min-width: 120px; justify-content: center; }
980
  .delete-button { background-color: #f56565; }
981
  .delete-button:hover { background-color: #e53e3e; }
982
- .add-button { background-color: #F472B6; } /* Main Pink */
983
- .add-button:hover { background-color: #DB2777; } /* Darker Pink */
984
  .item-list { display: grid; gap: 20px; }
985
- .item { background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.07); border: 1px solid #FCE7F3; }
986
- .item p { margin: 5px 0; font-size: 0.9rem; color: #6b4d5d; }
987
- .item strong { color: #4d333f; }
988
  .item .description { font-size: 0.85rem; color: #a0aec0; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
989
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
990
- .item-actions button:not(.delete-button) { background-color: #F472B6; }
991
- .item-actions button:not(.delete-button):hover { background-color: #DB2777; }
992
- .edit-form-container { margin-top: 15px; padding: 20px; background: #fff0f5; border: 1px dashed #fbcfe8; border-radius: 6px; display: none; }
993
- details { background-color: #fff7fa; border: 1px solid #FBCFE8; border-radius: 8px; margin-bottom: 20px; }
994
- details > summary { cursor: pointer; font-weight: 600; color: #DB2777; display: block; padding: 15px; border-bottom: 1px solid #FBCFE8; list-style: none; position: relative; }
995
- details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; color: #F472B6; }
996
  details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
997
- details[open] > summary { border-bottom: 1px solid #FBCFE8; }
998
  details .form-content { padding: 20px; }
999
  .color-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
1000
  .color-input-group input { flex-grow: 1; margin: 0; }
1001
  .remove-color-btn { background-color: #f56565; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
1002
  .remove-color-btn:hover { background-color: #e53e3e; }
1003
- .add-color-btn { background-color: #f9a8d4; color: #DB2777; } /* Lighter pink, darker text */
1004
- .add-color-btn:hover { background-color: #fbcfe8; }
1005
- .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; border: 1px solid #FBCFE8; object-fit: cover;}
1006
  .sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
1007
- .download-hf-button { background-color: #a0aec0; } /* Grey */
1008
  .download-hf-button:hover { background-color: #718096; }
1009
  .flex-container { display: flex; flex-wrap: wrap; gap: 20px; }
1010
  .flex-item { flex: 1; min-width: 350px; }
1011
  .message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-size: 0.9rem;}
1012
- .message.success { background-color: #f0fff4; color: #155724; border: 1px solid #c3e6cb;} /* Keep success green */
1013
- .message.error { background-color: #fff5f5; color: #721c24; border: 1px solid #f5c6cb;} /* Keep error red */
1014
- .message.warning { background-color: #fffaf0; color: #856404; border: 1px solid #ffeeba; } /* Keep warning yellow */
1015
  .status-indicator { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 0.8rem; font-weight: 500; margin-left: 10px; vertical-align: middle; }
1016
- .status-indicator.in-stock { background-color: #c6f6d5; color: #2f855a; } /* Keep green */
1017
- .status-indicator.out-of-stock { background-color: #fed7d7; color: #c53030; } /* Keep red */
1018
- .status-indicator.top-product { background-color: #feebc8; color: #9c4221; margin-left: 5px;} /* Keep gold */
1019
  </style>
1020
  </head>
1021
  <body>
1022
  <div class="container">
1023
  <div class="header">
1024
  <div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
1025
- <img src="https://cdn-avatars.huggingface.co/v1/production/uploads/67c280ccb9d3dfdee58ecfdd/p2TVtSc6UQr0De1jDYBLk.jpeg" alt="Aikas_optom Logo" style="height: 40px; width: auto; border-radius: 4px;">
1026
- <h1><i class="fas fa-tools"></i> Админ-панель Aikas_optom</h1>
1027
  </div>
1028
- <a href="{{ url_for('catalog') }}" class="button" style="background-color: #EC4899;"><i class="fas fa-store"></i> Перейти в каталог</a>
1029
  </div>
1030
-
1031
-
1032
  {% with messages = get_flashed_messages(with_categories=true) %}
1033
  {% if messages %}
1034
  {% for category, message in messages %}
@@ -1036,7 +927,6 @@ ADMIN_TEMPLATE = '''
1036
  {% endfor %}
1037
  {% endif %}
1038
  {% endwith %}
1039
-
1040
  <div class="section">
1041
  <h2><i class="fas fa-sync-alt"></i> Синхронизация с Датацентром</h2>
1042
  <div class="sync-buttons">
@@ -1047,9 +937,8 @@ ADMIN_TEMPLATE = '''
1047
  <button type="submit" class="button download-hf-button" title="Скачать файлы (перезапишет локальные)"><i class="fas fa-download"></i> Скачать БД</button>
1048
  </form>
1049
  </div>
1050
- <p style="font-size: 0.85rem; color: #a0aec0;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
1051
  </div>
1052
-
1053
  <div class="flex-container">
1054
  <div class="flex-item">
1055
  <div class="section">
@@ -1065,7 +954,6 @@ ADMIN_TEMPLATE = '''
1065
  </form>
1066
  </div>
1067
  </details>
1068
-
1069
  <h3>Существующие категории:</h3>
1070
  {% if categories %}
1071
  <div class="item-list">
@@ -1085,7 +973,6 @@ ADMIN_TEMPLATE = '''
1085
  {% endif %}
1086
  </div>
1087
  </div>
1088
-
1089
  <div class="flex-item">
1090
  <div class="section">
1091
  <h2><i class="fas fa-info-circle"></i> Информация</h2>
@@ -1094,7 +981,6 @@ ADMIN_TEMPLATE = '''
1094
  </div>
1095
  </div>
1096
  </div>
1097
-
1098
  <div class="section">
1099
  <h2><i class="fas fa-box-open"></i> Управление товарами</h2>
1100
  <details>
@@ -1139,7 +1025,6 @@ ADMIN_TEMPLATE = '''
1139
  </form>
1140
  </div>
1141
  </details>
1142
-
1143
  <h3>Список товаров:</h3>
1144
  {% if products %}
1145
  <div class="item-list">
@@ -1156,7 +1041,7 @@ ADMIN_TEMPLATE = '''
1156
  {% endif %}
1157
  </div>
1158
  <div style="flex-grow: 1;">
1159
- <h3 style="margin-top: 0; margin-bottom: 5px; color: #4d333f;">
1160
  {{ product['name'] }}
1161
  {% if product.get('in_stock', True) %}
1162
  <span class="status-indicator in-stock">В наличии</span>
@@ -1173,11 +1058,10 @@ ADMIN_TEMPLATE = '''
1173
  {% set colors = product.get('colors', []) %}
1174
  <p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
1175
  {% if product.get('photos') and product['photos']|length > 1 %}
1176
- <p style="font-size: 0.8rem; color: #a0aec0;">(Всего фото: {{ product['photos']|length }})</p>
1177
  {% endif %}
1178
  </div>
1179
  </div>
1180
-
1181
  <div class="item-actions">
1182
  <button type="button" class="button" onclick="toggleEditForm('edit-form-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button>
1183
  <form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить товар \'{{ product['name'] }}\'?');">
@@ -1186,7 +1070,6 @@ ADMIN_TEMPLATE = '''
1186
  <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
1187
  </form>
1188
  </div>
1189
-
1190
  <div id="edit-form-{{ loop.index0 }}" class="edit-form-container">
1191
  <h4><i class="fas fa-edit"></i> Редактирование: {{ product['name'] }}</h4>
1192
  <form method="POST" enctype="multipart/form-data">
@@ -1229,7 +1112,7 @@ ADMIN_TEMPLATE = '''
1229
  {% endfor %}
1230
  {% else %}
1231
  <div class="color-input-group">
1232
- <input type="text" name="colors" placeholder="Например: Красный">
1233
  <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1234
  </div>
1235
  {% endif %}
@@ -1255,9 +1138,7 @@ ADMIN_TEMPLATE = '''
1255
  <p>Товаров пока нет.</p>
1256
  {% endif %}
1257
  </div>
1258
-
1259
  </div>
1260
-
1261
  <script>
1262
  function toggleEditForm(formId) {
1263
  const formContainer = document.getElementById(formId);
@@ -1265,7 +1146,6 @@ ADMIN_TEMPLATE = '''
1265
  formContainer.style.display = formContainer.style.display === 'none' || formContainer.style.display === '' ? 'block' : 'none';
1266
  }
1267
  }
1268
-
1269
  function addColorInput(containerId) {
1270
  const container = document.getElementById(containerId);
1271
  if (container) {
@@ -1282,13 +1162,11 @@ ADMIN_TEMPLATE = '''
1282
  }
1283
  }
1284
  }
1285
-
1286
  function removeColorInput(button) {
1287
  const group = button.closest('.color-input-group');
1288
  if (group) {
1289
  const container = group.parentNode;
1290
  group.remove();
1291
- // Add back a placeholder if the last one was removed
1292
  if (container && container.children.length === 0) {
1293
  const placeholderGroup = document.createElement('div');
1294
  placeholderGroup.className = 'color-input-group';
@@ -1306,18 +1184,13 @@ ADMIN_TEMPLATE = '''
1306
  </body>
1307
  </html>
1308
  '''
1309
-
1310
- # --- Flask Routes ---
1311
-
1312
  @app.route('/')
1313
  def catalog():
1314
  data = load_data()
1315
  all_products = data.get('products', [])
1316
  categories = sorted(data.get('categories', []))
1317
-
1318
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1319
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
1320
-
1321
  return render_template_string(
1322
  CATALOG_TEMPLATE,
1323
  products=products_sorted,
@@ -1326,37 +1199,30 @@ def catalog():
1326
  store_address=STORE_ADDRESS,
1327
  currency_code=CURRENCY_CODE
1328
  )
1329
-
1330
  @app.route('/product/<int:index>')
1331
  def product_detail(index):
1332
  data = load_data()
1333
  all_products = data.get('products', [])
1334
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1335
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
1336
-
1337
  try:
1338
  product = products_sorted[index]
1339
  except IndexError:
1340
  logging.warning(f"Attempted access to non-existent or out-of-stock product with index {index}")
1341
  return "Товар не найден или отсутствует в наличии.", 404
1342
-
1343
  return render_template_string(
1344
  PRODUCT_DETAIL_TEMPLATE,
1345
  product=product,
1346
  repo_id=REPO_ID,
1347
  currency_code=CURRENCY_CODE
1348
  )
1349
-
1350
  @app.route('/create_order', methods=['POST'])
1351
  def create_order():
1352
  order_data = request.get_json()
1353
-
1354
  if not order_data or 'cart' not in order_data or not order_data['cart']:
1355
  logging.warning("Create order request missing cart data or cart is empty.")
1356
  return jsonify({"error": "Корзина пуста или не передана."}), 400
1357
-
1358
  cart_items = order_data['cart']
1359
-
1360
  total_price = 0
1361
  processed_cart = []
1362
  for item in cart_items:
@@ -1380,48 +1246,40 @@ def create_order():
1380
  except (ValueError, TypeError) as e:
1381
  logging.error(f"Invalid price/quantity in cart item: {item}. Error: {e}")
1382
  return jsonify({"error": "Неверная цена или количество в товаре."}), 400
1383
-
1384
  order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
1385
  order_timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1386
-
1387
  new_order = {
1388
  "id": order_id,
1389
  "created_at": order_timestamp,
1390
  "cart": processed_cart,
1391
  "total_price": round(total_price, 2),
1392
- "user_info": None, # Explicitly set to None as users are anonymous
1393
  "status": "new"
1394
  }
1395
-
1396
  try:
1397
  data = load_data()
1398
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1399
  data['orders'] = {}
1400
-
1401
  data['orders'][order_id] = new_order
1402
  save_data(data)
1403
  logging.info(f"Order {order_id} created successfully (anonymously).")
1404
  return jsonify({"order_id": order_id}), 201
1405
-
1406
  except Exception as e:
1407
  logging.error(f"Failed to save order {order_id}: {e}", exc_info=True)
1408
  return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
1409
-
1410
  @app.route('/order/<order_id>')
1411
  def view_order(order_id):
1412
  data = load_data()
1413
  order = data.get('orders', {}).get(order_id)
1414
-
1415
  if order:
1416
  logging.info(f"Displaying order {order_id}")
1417
  else:
1418
  logging.warning(f"Order {order_id} not found.")
1419
-
1420
  return render_template_string(ORDER_TEMPLATE,
1421
  order=order,
1422
  repo_id=REPO_ID,
1423
- currency_code=CURRENCY_CODE)
1424
-
1425
  @app.route('/admin', methods=['GET', 'POST'])
1426
  def admin():
1427
  data = load_data()
@@ -1429,11 +1287,9 @@ def admin():
1429
  categories = data.get('categories', [])
1430
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1431
  data['orders'] = {}
1432
-
1433
  if request.method == 'POST':
1434
  action = request.form.get('action')
1435
  logging.info(f"Admin action received: {action}")
1436
-
1437
  try:
1438
  if action == 'add_category':
1439
  category_name = request.form.get('category_name', '').strip()
@@ -1449,7 +1305,6 @@ def admin():
1449
  else:
1450
  logging.warning(f"Category '{category_name}' already exists.")
1451
  flash(f"Категория '{category_name}' уже существует.", 'error')
1452
-
1453
  elif action == 'delete_category':
1454
  category_to_delete = request.form.get('category_name')
1455
  if category_to_delete and category_to_delete in categories:
@@ -1467,7 +1322,6 @@ def admin():
1467
  else:
1468
  logging.warning(f"Attempted to delete non-existent or empty category: {category_to_delete}")
1469
  flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
1470
-
1471
  elif action == 'add_product':
1472
  name = request.form.get('name', '').strip()
1473
  price_str = request.form.get('price', '').replace(',', '.')
@@ -1477,18 +1331,15 @@ def admin():
1477
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1478
  in_stock = 'in_stock' in request.form
1479
  is_top = 'is_top' in request.form
1480
-
1481
  if not name or not price_str:
1482
  flash("Название и цена товара обязательны.", 'error')
1483
  return redirect(url_for('admin'))
1484
-
1485
  try:
1486
  price = round(float(price_str), 2)
1487
  if price < 0: price = 0
1488
  except ValueError:
1489
  flash("Неверный формат цены.", 'error')
1490
  return redirect(url_for('admin'))
1491
-
1492
  photos_list = []
1493
  if photos_files and HF_TOKEN_WRITE:
1494
  uploads_dir = 'uploads_temp'
@@ -1508,7 +1359,6 @@ def admin():
1508
  logging.warning(f"Skipping non-image file upload: {photo.filename}")
1509
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1510
  continue
1511
-
1512
  safe_name = secure_filename(name.replace(' ', '_'))[:50]
1513
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1514
  temp_path = os.path.join(uploads_dir, photo_filename)
@@ -1541,8 +1391,6 @@ def admin():
1541
  logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}")
1542
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
1543
  flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
1544
-
1545
-
1546
  new_product = {
1547
  'name': name, 'price': price, 'description': description,
1548
  'category': category if category in categories else 'Без категории',
@@ -1554,25 +1402,21 @@ def admin():
1554
  save_data(data)
1555
  logging.info(f"Product '{name}' added.")
1556
  flash(f"Товар '{name}' успешно добавлен.", 'success')
1557
-
1558
  elif action == 'edit_product':
1559
  index_str = request.form.get('index')
1560
  if index_str is None:
1561
  flash("Ошибка редактирования: индекс товара не передан.", 'error')
1562
  return redirect(url_for('admin'))
1563
-
1564
  try:
1565
  index = int(index_str)
1566
  if not (0 <= index < len(products)):
1567
  raise IndexError("Product index out of range")
1568
  product_to_edit = products[index]
1569
  original_name = product_to_edit.get('name', 'N/A')
1570
-
1571
  except (ValueError, IndexError):
1572
  flash(f"Ошибка редактирования: неверный индекс товара '{index_str}'.", 'error')
1573
  logging.error(f"Invalid index '{index_str}' for editing. Product list length: {len(products)}")
1574
  return redirect(url_for('admin'))
1575
-
1576
  product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
1577
  price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
1578
  product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
@@ -1581,7 +1425,6 @@ def admin():
1581
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1582
  product_to_edit['in_stock'] = 'in_stock' in request.form
1583
  product_to_edit['is_top'] = 'is_top' in request.form
1584
-
1585
  try:
1586
  price = round(float(price_str), 2)
1587
  if price < 0: price = 0
@@ -1589,7 +1432,6 @@ def admin():
1589
  except ValueError:
1590
  logging.warning(f"Invalid price format '{price_str}' during edit of product {original_name}. Price not changed.")
1591
  flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
1592
-
1593
  photos_files = request.files.getlist('photos')
1594
  if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE:
1595
  uploads_dir = 'uploads_temp'
@@ -1611,7 +1453,6 @@ def admin():
1611
  logging.warning(f"Skipping non-image file upload during edit: {photo.filename}")
1612
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1613
  continue
1614
-
1615
  safe_name = secure_filename(product_to_edit['name'].replace(' ', '_'))[:50]
1616
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1617
  temp_path = os.path.join(uploads_dir, photo_filename)
@@ -1635,7 +1476,6 @@ def admin():
1635
  os.rmdir(uploads_dir)
1636
  except OSError as e:
1637
  logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}")
1638
-
1639
  if new_photos_list:
1640
  logging.info(f"New photo list for product {product_to_edit['name']} generated.")
1641
  old_photos = product_to_edit.get('photos', [])
@@ -1659,13 +1499,11 @@ def admin():
1659
  flash("Не удалось загрузить новые фотографии (возможно, неверный формат).", "error")
1660
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
1661
  flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning")
1662
-
1663
  products[index] = product_to_edit
1664
  data['products'] = products
1665
  save_data(data)
1666
  logging.info(f"Product '{original_name}' (index {index}) updated to '{product_to_edit['name']}'.")
1667
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
1668
-
1669
  elif action == 'delete_product':
1670
  index_str = request.form.get('index')
1671
  if index_str is None:
@@ -1676,7 +1514,6 @@ def admin():
1676
  if not (0 <= index < len(products)): raise IndexError("Product index out of range")
1677
  deleted_product = products.pop(index)
1678
  product_name = deleted_product.get('name', 'N/A')
1679
-
1680
  photos_to_delete = deleted_product.get('photos', [])
1681
  if photos_to_delete and HF_TOKEN_WRITE:
1682
  logging.info(f"Attempting to delete photos for product '{product_name}' from HF: {photos_to_delete}")
@@ -1696,8 +1533,6 @@ def admin():
1696
  elif photos_to_delete and not HF_TOKEN_WRITE:
1697
  logging.warning(f"HF_TOKEN (write) not set. Cannot delete photos {photos_to_delete} for deleted product '{product_name}'.")
1698
  flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
1699
-
1700
-
1701
  data['products'] = products
1702
  save_data(data)
1703
  logging.info(f"Product '{product_name}' (original index {index}) deleted.")
@@ -1705,23 +1540,17 @@ def admin():
1705
  except (ValueError, IndexError):
1706
  flash(f"Ошибка удаления: неверный индекс товара '{index_str}'.", 'error')
1707
  logging.error(f"Invalid index '{index_str}' for deletion. Product list length: {len(products)}")
1708
-
1709
  else:
1710
  logging.warning(f"Received unknown admin action: {action}")
1711
  flash(f"Неизвестное действие: {action}", 'warning')
1712
-
1713
  return redirect(url_for('admin'))
1714
-
1715
  except Exception as e:
1716
  logging.error(f"Error processing admin action '{action}': {e}", exc_info=True)
1717
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
1718
  return redirect(url_for('admin'))
1719
-
1720
- # --- GET request ---
1721
  current_data = load_data()
1722
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
1723
  display_categories = sorted(current_data.get('categories', []))
1724
-
1725
  return render_template_string(
1726
  ADMIN_TEMPLATE,
1727
  products=display_products,
@@ -1729,7 +1558,6 @@ def admin():
1729
  repo_id=REPO_ID,
1730
  currency_code=CURRENCY_CODE
1731
  )
1732
-
1733
  @app.route('/force_upload', methods=['POST'])
1734
  def force_upload():
1735
  logging.info("Forcing upload to Hugging Face...")
@@ -1740,39 +1568,30 @@ def force_upload():
1740
  logging.error(f"Error during forced upload: {e}", exc_info=True)
1741
  flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error')
1742
  return redirect(url_for('admin'))
1743
-
1744
  @app.route('/force_download', methods=['POST'])
1745
  def force_download():
1746
  logging.info("Forcing download from Hugging Face...")
1747
  try:
1748
  if download_db_from_hf():
1749
  flash("Данные успешно скачаны с Hugging Face. Локальные файлы обновлены.", 'success')
1750
- load_data() # Reload data in memory after download
1751
  else:
1752
  flash("Не удалось скачать данные с Hugging Face после нескольких попыток. Проверьте логи.", 'error')
1753
  except Exception as e:
1754
  logging.error(f"Error during forced download: {e}", exc_info=True)
1755
  flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
1756
  return redirect(url_for('admin'))
1757
-
1758
-
1759
- # --- App Initialization ---
1760
-
1761
  if __name__ == '__main__':
1762
  logging.info("Application starting up. Performing initial data load/download...")
1763
  download_db_from_hf()
1764
  load_data()
1765
  logging.info("Initial data load complete.")
1766
-
1767
  if HF_TOKEN_WRITE:
1768
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1769
  backup_thread.start()
1770
  logging.info("Periodic backup thread started.")
1771
  else:
1772
  logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).")
1773
-
1774
  port = int(os.environ.get('PORT', 7860))
1775
  logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
1776
  app.run(debug=False, host='0.0.0.0', port=port)
1777
-
1778
- # --- END OF FILE app.py ---
 
1
 
 
 
2
  from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify
3
  import json
4
  import os
 
12
  from dotenv import load_dotenv
13
  import requests
14
  import uuid
 
15
  load_dotenv()
 
16
  app = Flask(__name__)
17
  app.secret_key = 'your_unique_secret_key_soola_cosmetics_67890_no_login'
18
  DATA_FILE = 'data.json'
 
 
19
  SYNC_FILES = [DATA_FILE]
 
20
  REPO_ID = "Kgshop/nizhbel"
21
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
22
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
 
23
  STORE_ADDRESS = "Рынок Кербент, 6 ряд , 3 контейнер / 5 ряд 25 контейнер "
 
24
  CURRENCY_CODE = 'KGS'
25
  CURRENCY_NAME = 'Кыргызский сом'
 
26
  DOWNLOAD_RETRIES = 3
27
+ DOWNLOAD_DELAY = 5
28
+ WHATSAPP_NUMBER = "996509455959"
29
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
 
30
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
31
  if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
32
  logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
 
33
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
 
34
  files_to_download = [specific_file] if specific_file else SYNC_FILES
35
  logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
36
  all_successful = True
 
37
  for file_name in files_to_download:
38
  success = False
39
  for attempt in range(retries + 1):
 
74
  logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
75
  except Exception as e:
76
  logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
 
77
  if attempt < retries:
78
  time.sleep(delay)
 
79
  if not success:
80
  logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
81
  all_successful = False
 
82
  logging.info(f"Download process finished. Overall success: {all_successful}")
83
  return all_successful
 
84
  def upload_db_to_hf(specific_file=None):
85
  if not HF_TOKEN_WRITE:
86
  logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.")
 
89
  api = HfApi()
90
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
91
  logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
 
92
  for file_name in files_to_upload:
93
  if os.path.exists(file_name):
94
  try:
 
108
  logging.info("Finished uploading files to HF.")
109
  except Exception as e:
110
  logging.error(f"General error during Hugging Face upload initialization or process: {e}", exc_info=True)
 
111
  def periodic_backup():
112
  backup_interval = 1800
113
  logging.info(f"Setting up periodic backup every {backup_interval} seconds.")
 
116
  logging.info("Starting periodic backup...")
117
  upload_db_to_hf()
118
  logging.info("Periodic backup finished.")
 
 
 
119
  def load_data():
120
  default_data = {'products': [], 'categories': [], 'orders': {}}
121
  try:
 
133
  logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
134
  except json.JSONDecodeError:
135
  logging.error(f"Error decoding JSON in local {DATA_FILE}. File might be corrupt. Attempting download.")
 
136
  if download_db_from_hf(specific_file=DATA_FILE):
137
  try:
138
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
 
164
  except Exception as create_e:
165
  logging.error(f"Failed to create empty local file {DATA_FILE}: {create_e}")
166
  return default_data
 
167
  def save_data(data):
168
  try:
169
  if not isinstance(data, dict):
 
172
  if 'products' not in data: data['products'] = []
173
  if 'categories' not in data: data['categories'] = []
174
  if 'orders' not in data: data['orders'] = {}
 
175
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
176
  json.dump(data, file, ensure_ascii=False, indent=4)
177
  logging.info(f"Data successfully saved to {DATA_FILE}")
178
  upload_db_to_hf(specific_file=DATA_FILE)
179
  except Exception as e:
180
  logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
 
 
 
181
  CATALOG_TEMPLATE = '''
182
  <!DOCTYPE html>
183
  <html lang="ru">
184
  <head>
185
  <meta charset="UTF-8">
186
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
187
+ <title>Meka Shop - Каталог</title>
188
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
189
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
190
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
191
  <style>
192
  * { margin: 0; padding: 0; box-sizing: border-box; }
193
+ body { font-family: 'Poppins', sans-serif; background: #d7f7e0; color: #1a362d; line-height: 1.6; transition: background 0.3s, color 0.3s; }
194
+ body.dark-mode { background: #1a362d; color: #d7f7e0; }
195
  .container { max-width: 1300px; margin: 0 auto; padding: 20px; }
196
+ .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #9cdbb0; }
197
+ body.dark-mode .header { border-bottom-color: #4f6a5a; }
198
+ .header h1 { font-size: 1.8rem; font-weight: 600; color: #4CAF50; }
199
+ .theme-toggle { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #7ed99c; transition: color 0.3s ease; }
200
+ .theme-toggle:hover { color: #388E3C; }
201
+ body.dark-mode .theme-toggle { color: #a7f3d0; }
202
+ body.dark-mode .theme-toggle:hover { color: #d7f7e0; }
203
+ .store-address { padding: 15px; text-align: center; background-color: #ffffff; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); font-size: 1rem; color: #4f6a5a; }
204
+ body.dark-mode .store-address { background-color: #0f231c; color: #d7f7e0; }
205
  .filters-container { margin: 20px 0; display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
206
  .search-container { margin: 20px 0; text-align: center; }
207
+ #search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #9cdbb0; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: all 0.3s ease; }
208
+ body.dark-mode #search-input { background-color: #0f231c; border-color: #4f6a5a; color: #d7f7e0; }
209
+ #search-input:focus { border-color: #4CAF50; box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.2); }
210
+ body.dark-mode #search-input:focus { border-color: #388E3C; box-shadow: 0 0 0 3px rgba(56, 142, 60, 0.3); }
211
+ .category-filter { padding: 8px 16px; border: 1px solid #9cdbb0; border-radius: 20px; background-color: #fff; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.9rem; font-weight: 400; color: #388E3C; }
212
+ body.dark-mode .category-filter { background-color: #0f231c; border-color: #4f6a5a; color: #a7f3d0; }
213
+ .category-filter.active, .category-filter:hover { background-color: #4CAF50; color: white; border-color: #4CAF50; box-shadow: 0 2px 10px rgba(76, 175, 80, 0.3); }
214
+ body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { background-color: #388E3C; border-color: #388E3C; color: #1a362d; box-shadow: 0 2px 10px rgba(56, 142, 60, 0.4); }
215
  .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 20px; padding: 10px; }
216
  @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
217
  @media (min-width: 900px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } }
218
+ .product { background: #fff; border-radius: 15px; padding: 0; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; height: 100%; border: 1px solid #c1e7cc;}
219
+ body.dark-mode .product { background: #0f231c; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); border-color: #4f6a5a; }
 
220
  .product:hover { transform: translateY(-5px) scale(1.02); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); }
221
  body.dark-mode .product:hover { box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); }
222
  .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; }
223
  .product-image img { max-width: 100%; max-height: 100%; object-fit: contain; transition: transform 0.3s ease; }
224
  .product-info { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; justify-content: center; }
225
+ .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: #1a362d; }
226
+ body.dark-mode .product h2 { color: #d7f7e0; }
227
+ .product-price { font-size: 1.2rem; color: #388E3C; font-weight: 700; text-align: center; margin: 5px 0; }
228
+ body.dark-mode .product-price { color: #a7f3d0; }
229
+ .product-description { font-size: 0.85rem; color: #4f6a5a; text-align: center; margin-bottom: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
230
+ body.dark-mode .product-description { color: #c1e7cc; }
231
  .product-actions { padding: 0 15px 15px 15px; display: flex; flex-direction: column; gap: 8px; }
232
+ .product-button { display: block; width: 100%; padding: 10px; border: none; border-radius: 8px; background-color: #4CAF50; color: white; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); text-align: center; text-decoration: none; }
233
+ .product-button:hover { background-color: #388E3C; box-shadow: 0 4px 15px rgba(56, 142, 60, 0.4); transform: translateY(-2px); }
234
  .product-button i { margin-right: 5px; }
235
+ .add-to-cart { background-color: #4CAF50; }
236
+ .add-to-cart:hover { background-color: #388E3C; box-shadow: 0 4px 15px rgba(56, 142, 60, 0.4); }
237
+ #cart-button { position: fixed; bottom: 25px; right: 25px; background-color: #4CAF50; color: white; border: none; border-radius: 50%; width: 55px; height: 55px; font-size: 1.5rem; cursor: pointer; display: none; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(76, 175, 80, 0.4); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1000; }
238
  #cart-button .fa-shopping-cart { margin-right: 0; }
239
+ #cart-button span { position: absolute; top: -5px; right: -5px; background-color: #388E3C; color: white; border-radius: 50%; padding: 2px 6px; font-size: 0.7rem; font-weight: bold; }
240
  .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(5px); overflow-y: auto; }
241
  .modal-content { background: #ffffff; margin: 5% auto; padding: 25px; border-radius: 15px; width: 90%; max-width: 700px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); animation: slideIn 0.3s ease-out; position: relative; }
242
+ body.dark-mode .modal-content { background: #0f231c; color: #d7f7e0; }
243
  @keyframes slideIn { from { transform: translateY(-30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
244
  .close { position: absolute; top: 15px; right: 15px; font-size: 1.8rem; color: #aaa; cursor: pointer; transition: color 0.3s; line-height: 1; }
245
  .close:hover { color: #333; }
246
+ body.dark-mode .close { color: #c1e7cc; }
247
  body.dark-mode .close:hover { color: #ffffff; }
248
+ .modal-content h2 { margin-top: 0; margin-bottom: 20px; color: #4CAF50; display: flex; align-items: center; gap: 10px;}
249
+ body.dark-mode .modal-content h2 { color: #a7f3d0; }
250
+ .cart-item { display: grid; grid-template-columns: auto 1fr auto auto; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid #9cdbb0; }
251
+ body.dark-mode .cart-item { border-bottom-color: #4f6a5a; }
252
  .cart-item:last-child { border-bottom: none; }
253
  .cart-item img { width: 60px; height: 60px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; grid-column: 1; }
254
  .cart-item-details { grid-column: 2; }
255
  .cart-item-details strong { display: block; margin-bottom: 5px; font-size: 1rem; }
256
+ .cart-item-price { font-size: 0.9rem; color: #4f6a5a; }
257
+ body.dark-mode .cart-item-price { color: #c1e7cc; }
258
  .cart-item-total { font-weight: bold; text-align: right; grid-column: 3; font-size: 1rem;}
259
  .cart-item-remove { grid-column: 4; background:none; border:none; color:#f56565; cursor:pointer; font-size: 1.3em; padding: 5px; line-height: 1; }
260
  .cart-item-remove:hover { color: #c53030; }
261
+ .quantity-input, .color-select { width: 100%; max-width: 180px; padding: 10px; border: 1px solid #9cdbb0; border-radius: 8px; font-size: 1rem; margin: 10px 0; box-sizing: border-box; }
262
+ body.dark-mode .quantity-input, body.dark-mode .color-select { background-color: #1a362d; border-color: #4f6a5a; color: #d7f7e0; }
263
+ .cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #9cdbb0; padding-top: 15px; }
264
+ body.dark-mode .cart-summary { border-top-color: #4f6a5a; }
265
  .cart-summary strong { font-size: 1.2rem; }
266
  .cart-actions { margin-top: 25px; display: flex; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
267
  .cart-actions .product-button { width: auto; flex-grow: 1; }
268
+ .clear-cart { background-color: #a0aec0; }
269
  .clear-cart:hover { background-color: #718096; box-shadow: 0 4px 15px rgba(113, 128, 150, 0.4); }
270
+ .formulate-order-button { background-color: #4CAF50; }
271
+ .formulate-order-button:hover { background-color: #388E3C; box-shadow: 0 4px 15px rgba(56, 142, 60, 0.4); }
272
+ .notification { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background-color: #4CAF50; color: white; padding: 10px 20px; border-radius: 20px; box-shadow: 0 4px 10px rgba(0,0,0,0.2); z-index: 1002; opacity: 0; transition: opacity 0.5s ease; font-size: 0.9rem;}
273
  .notification.show { opacity: 1;}
274
  .no-results-message { grid-column: 1 / -1; text-align: center; padding: 40px; font-size: 1.1rem; color: #a0aec0; }
275
+ body.dark-mode .no-results-message { color: #c1e7cc; }
276
  .top-product-indicator { position: absolute; top: 8px; right: 8px; background-color: rgba(255, 215, 0, 0.8); color: #333; padding: 2px 6px; font-size: 0.7rem; border-radius: 4px; font-weight: bold; z-index: 10; backdrop-filter: blur(2px); }
277
  .product { position: relative; }
278
  </style>
 
281
  <div class="container">
282
  <div class="header">
283
  <div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
284
+ <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/Apple-logo.png/2560px-Apple-logo.png" alt="Meka Shop Logo" style="height: 40px; width: auto; border-radius: 4px;">
285
+ <h1>Meka Shop</h1>
286
  </div>
287
  <button class="theme-toggle" onclick="toggleTheme()" aria-label="Переключить тему">
288
  <i class="fas fa-moon"></i>
289
  </button>
290
  </div>
 
291
  <div class="store-address">Наш адрес: {{ store_address }}</div>
 
292
  <div class="filters-container">
293
  <button class="category-filter active" data-category="all">Все категории</button>
294
  {% for category in categories %}
295
  <button class="category-filter" data-category="{{ category }}">{{ category }}</button>
296
  {% endfor %}
297
  </div>
 
298
  <div class="search-container">
299
  <input type="text" id="search-input" placeholder="Поиск по названию или описанию...">
300
  </div>
 
301
  <div class="products-grid" id="products-grid">
302
  {% for product in products %}
303
  <div class="product"
 
334
  {% endif %}
335
  </div>
336
  </div>
 
337
  <div id="productModal" class="modal">
338
  <div class="modal-content">
339
  <span class="close" onclick="closeModal('productModal')" aria-label="Закрыть">×</span>
340
  <div id="modalContent">Загрузка...</div>
341
  </div>
342
  </div>
 
343
  <div id="quantityModal" class="modal">
344
  <div class="modal-content">
345
  <span class="close" onclick="closeModal('quantityModal')" aria-label="Закрыть">×</span>
 
351
  <button class="product-button add-to-cart" onclick="confirmAddToCart()"><i class="fas fa-check"></i> Добавить в корзину</button>
352
  </div>
353
  </div>
 
354
  <div id="cartModal" class="modal">
355
  <div class="modal-content">
356
  <span class="close" onclick="closeModal('cartModal')" aria-label="Закрыть">×</span>
 
369
  </div>
370
  </div>
371
  </div>
 
372
  <button id="cart-button" onclick="openCartModal()" aria-label="Открыть корзину">
373
  <i class="fas fa-shopping-cart"></i>
374
  <span id="cart-count">0</span>
375
  </button>
 
376
  <div id="notification-placeholder"></div>
 
377
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
378
  <script>
379
  const products = {{ products|tojson }};
 
381
  const currencyCode = '{{ currency_code }}';
382
  let selectedProductIndex = null;
383
  let cart = JSON.parse(localStorage.getItem('soolaCart') || '[]');
 
384
  function toggleTheme() {
385
  document.body.classList.toggle('dark-mode');
386
  const icon = document.querySelector('.theme-toggle i');
 
389
  icon.classList.toggle('fa-sun', isDarkMode);
390
  localStorage.setItem('soolaTheme', isDarkMode ? 'dark' : 'light');
391
  }
 
392
  function applyInitialTheme() {
393
  const savedTheme = localStorage.getItem('soolaTheme');
394
  if (savedTheme === 'dark') {
 
397
  if (icon) icon.classList.replace('fa-moon', 'fa-sun');
398
  }
399
  }
 
400
  function openModal(index) {
401
  loadProductDetails(index);
402
  const modal = document.getElementById('productModal');
 
405
  document.body.style.overflow = 'hidden';
406
  }
407
  }
 
408
  function closeModal(modalId) {
409
  const modal = document.getElementById(modalId);
410
  if (modal) {
 
415
  document.body.style.overflow = 'auto';
416
  }
417
  }
 
418
  function loadProductDetails(index) {
419
  const modalContent = document.getElementById('modalContent');
420
  if (!modalContent) return;
 
433
  modalContent.innerHTML = `<p style="color: red; text-align:center; padding: 40px;">Не удалось загрузить информацию о товаре. ${error.message}</p>`;
434
  });
435
  }
 
436
  function initializeSwiper() {
437
  const swiperContainer = document.querySelector('#productModal .swiper-container');
438
  if (swiperContainer) {
 
448
  });
449
  }
450
  }
 
451
  function openQuantityModal(index) {
452
  selectedProductIndex = index;
453
  const product = products[index];
 
456
  alert("Ошибка: товар не найден.");
457
  return;
458
  }
 
459
  const colorSelect = document.getElementById('colorSelect');
460
  const colorLabel = document.querySelector('label[for="colorSelect"]');
461
  colorSelect.innerHTML = '';
 
462
  const validColors = product.colors ? product.colors.filter(c => c && c.trim() !== "") : [];
 
463
  if (validColors.length > 0) {
464
  validColors.forEach(color => {
465
  const option = document.createElement('option');
 
473
  colorSelect.style.display = 'none';
474
  if(colorLabel) colorLabel.style.display = 'none';
475
  }
 
476
  document.getElementById('quantityInput').value = 1;
477
  const modal = document.getElementById('quantityModal');
478
  if(modal) {
 
480
  document.body.style.overflow = 'hidden';
481
  }
482
  }
 
483
  function confirmAddToCart() {
484
  if (selectedProductIndex === null) return;
 
485
  const quantityInput = document.getElementById('quantityInput');
486
  const quantity = parseInt(quantityInput.value);
487
  const colorSelect = document.getElementById('colorSelect');
488
  const color = colorSelect.style.display !== 'none' && colorSelect.value ? colorSelect.value : 'N/A';
 
489
  if (isNaN(quantity) || quantity <= 0) {
490
  alert("Пожалуйста, укажите корректное количество (больше 0).");
491
  quantityInput.focus();
492
  return;
493
  }
 
494
  const product = products[selectedProductIndex];
495
  if (!product) {
496
  alert("Ошибка добавления: товар не найден.");
497
  return;
498
  }
499
+ const cartItemId = `${product.name}-${color}`;
 
500
  const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
 
501
  if (existingItemIndex > -1) {
502
  cart[existingItemIndex].quantity += quantity;
503
  } else {
 
510
  color: color
511
  });
512
  }
 
513
  localStorage.setItem('soolaCart', JSON.stringify(cart));
514
  closeModal('quantityModal');
515
  updateCartButton();
516
  showNotification(`${product.name} добавлен в корзину!`);
517
  }
 
518
  function updateCartButton() {
519
  const cartCountElement = document.getElementById('cart-count');
520
  const cartButton = document.getElementById('cart-button');
521
  if (!cartCountElement || !cartButton) return;
 
522
  let totalItems = 0;
523
  cart.forEach(item => { totalItems += item.quantity; });
 
524
  if (totalItems > 0) {
525
  cartCountElement.textContent = totalItems;
526
  cartButton.style.display = 'flex';
 
529
  cartButton.style.display = 'none';
530
  }
531
  }
 
532
  function openCartModal() {
533
  const cartContent = document.getElementById('cartContent');
534
  const cartTotalElement = document.getElementById('cartTotal');
535
  if (!cartContent || !cartTotalElement) return;
 
536
  let total = 0;
 
537
  if (cart.length === 0) {
538
  cartContent.innerHTML = '<p style="text-align: center; padding: 20px;">Ваша корзина пуста.</p>';
539
  cartTotalElement.textContent = '0.00';
 
545
  ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
546
  : 'https://via.placeholder.com/60x60.png?text=N/A';
547
  const colorText = item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
 
548
  return `
549
  <div class="cart-item">
550
  <img src="${photoUrl}" alt="${item.name}">
 
565
  document.body.style.overflow = 'hidden';
566
  }
567
  }
 
568
  function removeFromCart(itemId) {
569
  cart = cart.filter(item => item.id !== itemId);
570
  localStorage.setItem('soolaCart', JSON.stringify(cart));
571
  openCartModal();
572
  updateCartButton();
573
  }
 
574
  function clearCart() {
575
  if (confirm("Вы уверены, что хотите очистить корзину?")) {
576
  cart = [];
 
579
  updateCartButton();
580
  }
581
  }
 
582
  function formulateOrder() {
583
  if (cart.length === 0) {
584
  alert("Корзина пуста! Добавьте товары перед формированием заказа.");
585
  return;
586
  }
 
587
  const orderData = {
588
  cart: cart
589
  };
 
590
  const formulateButton = document.querySelector('.formulate-order-button');
591
  if (formulateButton) formulateButton.disabled = true;
 
592
  showNotification("Формируем заказ...", 5000);
 
593
  fetch('/create_order', {
594
  method: 'POST',
595
  headers: { 'Content-Type': 'application/json' },
 
618
  if (formulateButton) formulateButton.disabled = false;
619
  });
620
  }
 
 
621
  function filterProducts() {
622
  const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
623
  const activeCategoryButton = document.querySelector('.category-filter.active');
624
  const activeCategory = activeCategoryButton ? activeCategoryButton.dataset.category : 'all';
625
  const grid = document.getElementById('products-grid');
626
  let visibleProducts = 0;
 
627
  const existingNoResults = grid.querySelector('.no-results-message');
628
  if (existingNoResults) existingNoResults.remove();
 
629
  document.querySelectorAll('.products-grid .product').forEach(productElement => {
630
  const name = productElement.getAttribute('data-name');
631
  const description = productElement.getAttribute('data-description');
632
  const category = productElement.getAttribute('data-category');
 
633
  const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
634
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
 
635
  if (matchesSearch && matchesCategory) {
636
  productElement.style.display = 'flex';
637
  visibleProducts++;
 
639
  productElement.style.display = 'none';
640
  }
641
  });
 
642
  if (visibleProducts === 0 && products.length > 0) {
643
  const p = document.createElement('p');
644
  p.className = 'no-results-message';
 
651
  grid.appendChild(p);
652
  }
653
  }
 
654
  function setupFilters() {
655
  const searchInput = document.getElementById('search-input');
656
  const categoryFilters = document.querySelectorAll('.category-filter');
 
657
  if(searchInput) searchInput.addEventListener('input', filterProducts);
 
658
  categoryFilters.forEach(filter => {
659
  filter.addEventListener('click', function() {
660
  categoryFilters.forEach(f => f.classList.remove('active'));
 
664
  });
665
  filterProducts();
666
  }
 
667
  function showNotification(message, duration = 3000) {
668
  const placeholder = document.getElementById('notification-placeholder');
669
  if (!placeholder) {
 
677
  document.body.appendChild(newPlaceholder);
678
  placeholder = newPlaceholder;
679
  }
 
 
680
  const notification = document.createElement('div');
681
  notification.className = 'notification';
682
  notification.textContent = message;
683
  placeholder.appendChild(notification);
 
684
  void notification.offsetWidth;
 
685
  notification.classList.add('show');
 
686
  setTimeout(() => {
687
  notification.classList.remove('show');
688
  notification.addEventListener('transitionend', () => notification.remove());
689
  }, duration);
690
  }
 
691
  document.addEventListener('DOMContentLoaded', () => {
692
  applyInitialTheme();
693
  updateCartButton();
694
  setupFilters();
 
695
  window.addEventListener('click', function(event) {
696
  if (event.target.classList.contains('modal')) {
697
  closeModal(event.target.id);
698
  }
699
  });
 
700
  window.addEventListener('keydown', function(event) {
701
  if (event.key === 'Escape') {
702
  document.querySelectorAll('.modal[style*="display: block"]').forEach(modal => {
 
705
  }
706
  });
707
  });
 
708
  </script>
709
  </body>
710
  </html>
711
  '''
 
712
  PRODUCT_DETAIL_TEMPLATE = '''
713
  <div style="padding: 10px;">
714
+ <h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 15px; text-align: center; color: #4CAF50;">{{ product['name'] }}</h2>
715
  <div class="swiper-container" style="max-width: 450px; margin: 0 auto 20px; border-radius: 10px; overflow: hidden; background-color: #fff;">
716
  <div class="swiper-wrapper">
717
  {% if product.get('photos') and product['photos']|length > 0 %}
 
732
  </div>
733
  {% if product.get('photos') and product['photos']|length > 1 %}
734
  <div class="swiper-pagination" style="position: relative; bottom: 5px;"></div>
735
+ <div class="swiper-button-next" style="color: #4CAF50;"></div>
736
+ <div class="swiper-button-prev" style="color: #4CAF50;"></div>
737
  {% endif %}
738
  </div>
 
739
  <div style="margin-top: 20px; font-size: 1rem; line-height: 1.7;">
740
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
741
+ <p style="font-size: 1.2rem; font-weight: bold; color: #388E3C;"><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
742
  <p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\\n', '<br>')|safe }}</p>
743
  {% set colors = product.get('colors', []) %}
744
  {% if colors and colors|select('ne', '')|list|length > 0 %}
 
747
  </div>
748
  </div>
749
  '''
 
750
  ORDER_TEMPLATE = '''
751
  <!DOCTYPE html>
752
  <html lang="ru">
753
  <head>
754
  <meta charset="UTF-8">
755
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
756
+ <title>Заказ №{{ order.id }} - Meka Shop</title>
757
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
758
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
759
  <style>
760
+ body { font-family: 'Poppins', sans-serif; background: #d7f7e0; color: #1a362d; line-height: 1.6; padding: 20px; }
761
+ .container { max-width: 800px; margin: 20px auto; padding: 30px; background: #fff; border-radius: 15px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); border: 1px solid #c1e7cc; }
762
+ h1 { text-align: center; color: #4CAF50; margin-bottom: 25px; font-size: 1.8rem; font-weight: 600; }
763
+ h2 { color: #388E3C; margin-top: 30px; margin-bottom: 15px; font-size: 1.4rem; border-bottom: 1px solid #9cdbb0; padding-bottom: 8px;}
764
  .order-meta { font-size: 0.9rem; color: #a0aec0; margin-bottom: 20px; text-align: center; }
765
+ .order-item { display: grid; grid-template-columns: 60px 1fr auto; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid #c1e7cc; }
766
  .order-item:last-child { border-bottom: none; }
767
+ .order-item img { width: 60px; height: 60px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; border: 1px solid #c1e7cc;}
768
+ .item-details strong { display: block; margin-bottom: 5px; font-size: 1.05rem; color: #1a362d;}
769
+ .item-details span { font-size: 0.9rem; color: #4f6a5a; display: block;}
770
+ .item-total { font-weight: bold; text-align: right; font-size: 1rem; color: #388E3C;}
771
+ .order-summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid #4CAF50; text-align: right; }
772
  .order-summary p { margin-bottom: 10px; font-size: 1.1rem; }
773
+ .order-summary strong { font-size: 1.3rem; color: #4CAF50; }
774
+ .customer-info { margin-top: 30px; background-color: #fff; padding: 20px; border-radius: 8px; border: 1px solid #c1e7cc;}
775
  .customer-info p { margin-bottom: 8px; font-size: 0.95rem; }
776
+ .customer-info strong { color: #388E3C; }
777
  .actions { margin-top: 30px; text-align: center; }
778
+ .button { padding: 12px 25px; border: none; border-radius: 8px; background-color: #4CAF50; 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; }
779
+ .button:hover { background-color: #388E3C; }
780
  .button:active { transform: scale(0.98); }
781
  .button i { font-size: 1.2rem; }
782
+ .catalog-link { display: block; text-align: center; margin-top: 25px; color: #388E3C; text-decoration: none; font-size: 0.9rem; }
783
  .catalog-link:hover { text-decoration: underline; }
784
  .not-found { text-align: center; color: #c53030; font-size: 1.2rem; padding: 40px 0;}
785
  </style>
 
789
  {% if order %}
790
  <h1><i class="fas fa-receipt"></i> Ваш Заказ №{{ order.id }}</h1>
791
  <p class="order-meta">Дата создания: {{ order.created_at }}</p>
 
792
  <h2><i class="fas fa-shopping-bag"></i> Товары в заказе</h2>
793
  <div id="orderItems">
794
  {% for item in order.cart %}
 
804
  </div>
805
  {% endfor %}
806
  </div>
 
807
  <div class="order-summary">
808
  <p>Общая сумма товаров: <strong>{{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
809
  <p><strong>ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
810
  </div>
 
811
  <div class="customer-info">
812
  <h2><i class="fas fa-info-circle"></i> Статус заказа</h2>
813
  <p>Этот заказ был оформлен без входа в систему.</p>
814
  <p>Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.</p>
815
  </div>
 
816
  <div class="actions">
817
  <button class="button" onclick="sendOrderViaWhatsApp()"><i class="fab fa-whatsapp"></i> Отправить заказ</button>
818
  </div>
 
819
  <a href="{{ url_for('catalog') }}" class="catalog-link">← Вернуться в каталог</a>
 
820
  <script>
821
  function sendOrderViaWhatsApp() {
822
  const orderId = '{{ order.id }}';
823
  const orderUrl = `{{ request.url }}`;
824
+ const whatsappNumber = "{{ whatsapp_number }}";
825
+ let message = `Здравствуйте! Хочу подтвердить свой заказ на Meka Shop:%0A%0A`;
 
826
  message += `*Номер заказа:* ${orderId}%0A`;
827
  message += `*Ссылка на заказ:* ${encodeURIComponent(orderUrl)}%0A%0A`;
828
  message += `Пожалуйста, свяжитесь со мной для уточнения деталей оплаты и доставки.`;
 
829
  const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${message}`;
830
  window.open(whatsappUrl, '_blank');
831
  }
832
  </script>
 
833
  {% else %}
834
  <h1 style="color: #c53030;"><i class="fas fa-exclamation-triangle"></i> Ошибка</h1>
835
  <p class="not-found">Заказ с таким ID не найден.</p>
 
839
  </body>
840
  </html>
841
  '''
 
842
  ADMIN_TEMPLATE = '''
843
  <!DOCTYPE html>
844
  <html lang="ru">
845
  <head>
846
  <meta charset="UTF-8">
847
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
848
+ <title>Админ-панель - Meka Shop</title>
849
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
850
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
851
  <style>
852
+ body { font-family: 'Poppins', sans-serif; background-color: #c1e7cc; color: #1a362d; padding: 20px; line-height: 1.6; }
853
  .container { max-width: 1200px; margin: 0 auto; background-color: #fff; padding: 25px; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.05); }
854
+ .header { padding-bottom: 15px; margin-bottom: 25px; border-bottom: 1px solid #9cdbb0; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;}
855
+ h1, h2, h3 { font-weight: 600; color: #4CAF50; margin-bottom: 15px; }
856
  h1 { font-size: 1.8rem; }
857
  h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
858
+ h3 { font-size: 1.2rem; color: #388E3C; margin-top: 20px; }
859
+ .section { margin-bottom: 30px; padding: 20px; background-color: #d7f7e0; border: 1px solid #c1e7cc; border-radius: 8px; }
860
  form { margin-bottom: 20px; }
861
+ label { font-weight: 500; margin-top: 10px; display: block; color: #4f6a5a; font-size: 0.9rem;}
862
+ input[type="text"], input[type="number"], input[type="password"], input[type="tel"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #9cdbb0; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; background-color: #fff; }
863
+ input:focus, textarea:focus, select:focus { border-color: #4CAF50; outline: none; box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1); }
864
  textarea { min-height: 80px; resize: vertical; }
865
+ input[type="file"] { padding: 8px; background-color: #d7f7e0; cursor: pointer; border: 1px solid #9cdbb0;}
866
+ input[type="file"]::file-selector-button { padding: 5px 10px; border-radius: 4px; background-color: #c1e7cc; border: 1px solid #9cdbb0; cursor: pointer; margin-right: 10px;}
867
  input[type="checkbox"] { margin-right: 5px; vertical-align: middle; }
868
  label.inline-label { display: inline-block; margin-top: 10px; font-weight: normal; }
869
+ button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #4CAF50; color: white; font-weight: 500; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; margin-top: 15px; font-size: 0.95rem; display: inline-flex; align-items: center; gap: 5px; text-decoration: none; line-height: 1.2;}
870
+ button:hover, .button:hover { background-color: #388E3C; }
871
  button:active, .button:active { transform: scale(0.98); }
872
  button[type="submit"] { min-width: 120px; justify-content: center; }
873
  .delete-button { background-color: #f56565; }
874
  .delete-button:hover { background-color: #e53e3e; }
875
+ .add-button { background-color: #4CAF50; }
876
+ .add-button:hover { background-color: #388E3C; }
877
  .item-list { display: grid; gap: 20px; }
878
+ .item { background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.07); border: 1px solid #c1e7cc; }
879
+ .item p { margin: 5px 0; font-size: 0.9rem; color: #4f6a5a; }
880
+ .item strong { color: #1a362d; }
881
  .item .description { font-size: 0.85rem; color: #a0aec0; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
882
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
883
+ .item-actions button:not(.delete-button) { background-color: #4CAF50; }
884
+ .item-actions button:not(.delete-button):hover { background-color: #388E3C; }
885
+ .edit-form-container { margin-top: 15px; padding: 20px; background: #d7f7e0; border: 1px dashed #9cdbb0; border-radius: 6px; display: none; }
886
+ details { background-color: #d7f7e0; border: 1px solid #c1e7cc; border-radius: 8px; margin-bottom: 20px; }
887
+ details > summary { cursor: pointer; font-weight: 600; color: #388E3C; display: block; padding: 15px; border-bottom: 1px solid #9cdbb0; list-style: none; position: relative; }
888
+ details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; color: #4CAF50; }
889
  details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
890
+ details[open] > summary { border-bottom: 1px solid #9cdbb0; }
891
  details .form-content { padding: 20px; }
892
  .color-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
893
  .color-input-group input { flex-grow: 1; margin: 0; }
894
  .remove-color-btn { background-color: #f56565; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
895
  .remove-color-btn:hover { background-color: #e53e3e; }
896
+ .add-color-btn { background-color: #a7f3d0; color: #388E3C; }
897
+ .add-color-btn:hover { background-color: #c1e7cc; }
898
+ .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; border: 1px solid #9cdbb0; object-fit: cover;}
899
  .sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
900
+ .download-hf-button { background-color: #a0aec0; }
901
  .download-hf-button:hover { background-color: #718096; }
902
  .flex-container { display: flex; flex-wrap: wrap; gap: 20px; }
903
  .flex-item { flex: 1; min-width: 350px; }
904
  .message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-size: 0.9rem;}
905
+ .message.success { background-color: #f0fff4; color: #155724; border: 1px solid #c3e6cb;}
906
+ .message.error { background-color: #fff5f5; color: #721c24; border: 1px solid #f5c6cb;}
907
+ .message.warning { background-color: #fffaf0; color: #856404; border: 1px solid #ffeeba; }
908
  .status-indicator { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 0.8rem; font-weight: 500; margin-left: 10px; vertical-align: middle; }
909
+ .status-indicator.in-stock { background-color: #c6f6d5; color: #2f855a; }
910
+ .status-indicator.out-of-stock { background-color: #fed7d7; color: #c53030; }
911
+ .status-indicator.top-product { background-color: #feebc8; color: #9c4221; margin-left: 5px;}
912
  </style>
913
  </head>
914
  <body>
915
  <div class="container">
916
  <div class="header">
917
  <div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
918
+ <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/Apple-logo.png/2560px-Apple-logo.png" alt="Meka Shop Logo" style="height: 40px; width: auto; border-radius: 4px;">
919
+ <h1><i class="fas fa-tools"></i> Админ-панель Meka Shop</h1>
920
  </div>
921
+ <a href="{{ url_for('catalog') }}" class="button" style="background-color: #388E3C;"><i class="fas fa-store"></i> Перейти в каталог</a>
922
  </div>
 
 
923
  {% with messages = get_flashed_messages(with_categories=true) %}
924
  {% if messages %}
925
  {% for category, message in messages %}
 
927
  {% endfor %}
928
  {% endif %}
929
  {% endwith %}
 
930
  <div class="section">
931
  <h2><i class="fas fa-sync-alt"></i> Синхронизация с Датацентром</h2>
932
  <div class="sync-buttons">
 
937
  <button type="submit" class="button download-hf-button" title="Скачать файлы (перезапишет локальные)"><i class="fas fa-download"></i> Скачать БД</button>
938
  </form>
939
  </div>
940
+ <p style="font-size: 0.85rem; color: #4f6a5a;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
941
  </div>
 
942
  <div class="flex-container">
943
  <div class="flex-item">
944
  <div class="section">
 
954
  </form>
955
  </div>
956
  </details>
 
957
  <h3>Существующие категории:</h3>
958
  {% if categories %}
959
  <div class="item-list">
 
973
  {% endif %}
974
  </div>
975
  </div>
 
976
  <div class="flex-item">
977
  <div class="section">
978
  <h2><i class="fas fa-info-circle"></i> Информация</h2>
 
981
  </div>
982
  </div>
983
  </div>
 
984
  <div class="section">
985
  <h2><i class="fas fa-box-open"></i> Управление товарами</h2>
986
  <details>
 
1025
  </form>
1026
  </div>
1027
  </details>
 
1028
  <h3>Список товаров:</h3>
1029
  {% if products %}
1030
  <div class="item-list">
 
1041
  {% endif %}
1042
  </div>
1043
  <div style="flex-grow: 1;">
1044
+ <h3 style="margin-top: 0; margin-bottom: 5px; color: #1a362d;">
1045
  {{ product['name'] }}
1046
  {% if product.get('in_stock', True) %}
1047
  <span class="status-indicator in-stock">В наличии</span>
 
1058
  {% set colors = product.get('colors', []) %}
1059
  <p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
1060
  {% if product.get('photos') and product['photos']|length > 1 %}
1061
+ <p style="font-size: 0.8rem; color: #4f6a5a;">(Всего фото: {{ product['photos']|length }})</p>
1062
  {% endif %}
1063
  </div>
1064
  </div>
 
1065
  <div class="item-actions">
1066
  <button type="button" class="button" onclick="toggleEditForm('edit-form-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button>
1067
  <form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить товар \'{{ product['name'] }}\'?');">
 
1070
  <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
1071
  </form>
1072
  </div>
 
1073
  <div id="edit-form-{{ loop.index0 }}" class="edit-form-container">
1074
  <h4><i class="fas fa-edit"></i> Редактирование: {{ product['name'] }}</h4>
1075
  <form method="POST" enctype="multipart/form-data">
 
1112
  {% endfor %}
1113
  {% else %}
1114
  <div class="color-input-group">
1115
+ <input type="text" name="colors" placeholder="Например: Цвет">
1116
  <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1117
  </div>
1118
  {% endif %}
 
1138
  <p>Товаров пока нет.</p>
1139
  {% endif %}
1140
  </div>
 
1141
  </div>
 
1142
  <script>
1143
  function toggleEditForm(formId) {
1144
  const formContainer = document.getElementById(formId);
 
1146
  formContainer.style.display = formContainer.style.display === 'none' || formContainer.style.display === '' ? 'block' : 'none';
1147
  }
1148
  }
 
1149
  function addColorInput(containerId) {
1150
  const container = document.getElementById(containerId);
1151
  if (container) {
 
1162
  }
1163
  }
1164
  }
 
1165
  function removeColorInput(button) {
1166
  const group = button.closest('.color-input-group');
1167
  if (group) {
1168
  const container = group.parentNode;
1169
  group.remove();
 
1170
  if (container && container.children.length === 0) {
1171
  const placeholderGroup = document.createElement('div');
1172
  placeholderGroup.className = 'color-input-group';
 
1184
  </body>
1185
  </html>
1186
  '''
 
 
 
1187
  @app.route('/')
1188
  def catalog():
1189
  data = load_data()
1190
  all_products = data.get('products', [])
1191
  categories = sorted(data.get('categories', []))
 
1192
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1193
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
 
1194
  return render_template_string(
1195
  CATALOG_TEMPLATE,
1196
  products=products_sorted,
 
1199
  store_address=STORE_ADDRESS,
1200
  currency_code=CURRENCY_CODE
1201
  )
 
1202
  @app.route('/product/<int:index>')
1203
  def product_detail(index):
1204
  data = load_data()
1205
  all_products = data.get('products', [])
1206
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1207
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
 
1208
  try:
1209
  product = products_sorted[index]
1210
  except IndexError:
1211
  logging.warning(f"Attempted access to non-existent or out-of-stock product with index {index}")
1212
  return "Товар не найден или отсутствует в наличии.", 404
 
1213
  return render_template_string(
1214
  PRODUCT_DETAIL_TEMPLATE,
1215
  product=product,
1216
  repo_id=REPO_ID,
1217
  currency_code=CURRENCY_CODE
1218
  )
 
1219
  @app.route('/create_order', methods=['POST'])
1220
  def create_order():
1221
  order_data = request.get_json()
 
1222
  if not order_data or 'cart' not in order_data or not order_data['cart']:
1223
  logging.warning("Create order request missing cart data or cart is empty.")
1224
  return jsonify({"error": "Корзина пуста или не передана."}), 400
 
1225
  cart_items = order_data['cart']
 
1226
  total_price = 0
1227
  processed_cart = []
1228
  for item in cart_items:
 
1246
  except (ValueError, TypeError) as e:
1247
  logging.error(f"Invalid price/quantity in cart item: {item}. Error: {e}")
1248
  return jsonify({"error": "Неверная цена или количество в товаре."}), 400
 
1249
  order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
1250
  order_timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
 
1251
  new_order = {
1252
  "id": order_id,
1253
  "created_at": order_timestamp,
1254
  "cart": processed_cart,
1255
  "total_price": round(total_price, 2),
1256
+ "user_info": None,
1257
  "status": "new"
1258
  }
 
1259
  try:
1260
  data = load_data()
1261
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1262
  data['orders'] = {}
 
1263
  data['orders'][order_id] = new_order
1264
  save_data(data)
1265
  logging.info(f"Order {order_id} created successfully (anonymously).")
1266
  return jsonify({"order_id": order_id}), 201
 
1267
  except Exception as e:
1268
  logging.error(f"Failed to save order {order_id}: {e}", exc_info=True)
1269
  return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
 
1270
  @app.route('/order/<order_id>')
1271
  def view_order(order_id):
1272
  data = load_data()
1273
  order = data.get('orders', {}).get(order_id)
 
1274
  if order:
1275
  logging.info(f"Displaying order {order_id}")
1276
  else:
1277
  logging.warning(f"Order {order_id} not found.")
 
1278
  return render_template_string(ORDER_TEMPLATE,
1279
  order=order,
1280
  repo_id=REPO_ID,
1281
+ currency_code=CURRENCY_CODE,
1282
+ whatsapp_number=WHATSAPP_NUMBER)
1283
  @app.route('/admin', methods=['GET', 'POST'])
1284
  def admin():
1285
  data = load_data()
 
1287
  categories = data.get('categories', [])
1288
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1289
  data['orders'] = {}
 
1290
  if request.method == 'POST':
1291
  action = request.form.get('action')
1292
  logging.info(f"Admin action received: {action}")
 
1293
  try:
1294
  if action == 'add_category':
1295
  category_name = request.form.get('category_name', '').strip()
 
1305
  else:
1306
  logging.warning(f"Category '{category_name}' already exists.")
1307
  flash(f"Категория '{category_name}' уже существует.", 'error')
 
1308
  elif action == 'delete_category':
1309
  category_to_delete = request.form.get('category_name')
1310
  if category_to_delete and category_to_delete in categories:
 
1322
  else:
1323
  logging.warning(f"Attempted to delete non-existent or empty category: {category_to_delete}")
1324
  flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
 
1325
  elif action == 'add_product':
1326
  name = request.form.get('name', '').strip()
1327
  price_str = request.form.get('price', '').replace(',', '.')
 
1331
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1332
  in_stock = 'in_stock' in request.form
1333
  is_top = 'is_top' in request.form
 
1334
  if not name or not price_str:
1335
  flash("Название и цена товара обязательны.", 'error')
1336
  return redirect(url_for('admin'))
 
1337
  try:
1338
  price = round(float(price_str), 2)
1339
  if price < 0: price = 0
1340
  except ValueError:
1341
  flash("Неверный формат цены.", 'error')
1342
  return redirect(url_for('admin'))
 
1343
  photos_list = []
1344
  if photos_files and HF_TOKEN_WRITE:
1345
  uploads_dir = 'uploads_temp'
 
1359
  logging.warning(f"Skipping non-image file upload: {photo.filename}")
1360
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1361
  continue
 
1362
  safe_name = secure_filename(name.replace(' ', '_'))[:50]
1363
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1364
  temp_path = os.path.join(uploads_dir, photo_filename)
 
1391
  logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}")
1392
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
1393
  flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
 
 
1394
  new_product = {
1395
  'name': name, 'price': price, 'description': description,
1396
  'category': category if category in categories else 'Без категории',
 
1402
  save_data(data)
1403
  logging.info(f"Product '{name}' added.")
1404
  flash(f"Товар '{name}' успешно добавлен.", 'success')
 
1405
  elif action == 'edit_product':
1406
  index_str = request.form.get('index')
1407
  if index_str is None:
1408
  flash("Ошибка редактирования: индекс товара не передан.", 'error')
1409
  return redirect(url_for('admin'))
 
1410
  try:
1411
  index = int(index_str)
1412
  if not (0 <= index < len(products)):
1413
  raise IndexError("Product index out of range")
1414
  product_to_edit = products[index]
1415
  original_name = product_to_edit.get('name', 'N/A')
 
1416
  except (ValueError, IndexError):
1417
  flash(f"Ошибка редактирования: неверный индекс товара '{index_str}'.", 'error')
1418
  logging.error(f"Invalid index '{index_str}' for editing. Product list length: {len(products)}")
1419
  return redirect(url_for('admin'))
 
1420
  product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
1421
  price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
1422
  product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
 
1425
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1426
  product_to_edit['in_stock'] = 'in_stock' in request.form
1427
  product_to_edit['is_top'] = 'is_top' in request.form
 
1428
  try:
1429
  price = round(float(price_str), 2)
1430
  if price < 0: price = 0
 
1432
  except ValueError:
1433
  logging.warning(f"Invalid price format '{price_str}' during edit of product {original_name}. Price not changed.")
1434
  flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
 
1435
  photos_files = request.files.getlist('photos')
1436
  if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE:
1437
  uploads_dir = 'uploads_temp'
 
1453
  logging.warning(f"Skipping non-image file upload during edit: {photo.filename}")
1454
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1455
  continue
 
1456
  safe_name = secure_filename(product_to_edit['name'].replace(' ', '_'))[:50]
1457
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1458
  temp_path = os.path.join(uploads_dir, photo_filename)
 
1476
  os.rmdir(uploads_dir)
1477
  except OSError as e:
1478
  logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}")
 
1479
  if new_photos_list:
1480
  logging.info(f"New photo list for product {product_to_edit['name']} generated.")
1481
  old_photos = product_to_edit.get('photos', [])
 
1499
  flash("Не удалось загрузить новые фотографии (возможно, неверный формат).", "error")
1500
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
1501
  flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning")
 
1502
  products[index] = product_to_edit
1503
  data['products'] = products
1504
  save_data(data)
1505
  logging.info(f"Product '{original_name}' (index {index}) updated to '{product_to_edit['name']}'.")
1506
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
 
1507
  elif action == 'delete_product':
1508
  index_str = request.form.get('index')
1509
  if index_str is None:
 
1514
  if not (0 <= index < len(products)): raise IndexError("Product index out of range")
1515
  deleted_product = products.pop(index)
1516
  product_name = deleted_product.get('name', 'N/A')
 
1517
  photos_to_delete = deleted_product.get('photos', [])
1518
  if photos_to_delete and HF_TOKEN_WRITE:
1519
  logging.info(f"Attempting to delete photos for product '{product_name}' from HF: {photos_to_delete}")
 
1533
  elif photos_to_delete and not HF_TOKEN_WRITE:
1534
  logging.warning(f"HF_TOKEN (write) not set. Cannot delete photos {photos_to_delete} for deleted product '{product_name}'.")
1535
  flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
 
 
1536
  data['products'] = products
1537
  save_data(data)
1538
  logging.info(f"Product '{product_name}' (original index {index}) deleted.")
 
1540
  except (ValueError, IndexError):
1541
  flash(f"Ошибка удаления: неверный индекс товара '{index_str}'.", 'error')
1542
  logging.error(f"Invalid index '{index_str}' for deletion. Product list length: {len(products)}")
 
1543
  else:
1544
  logging.warning(f"Received unknown admin action: {action}")
1545
  flash(f"Неизвестное действие: {action}", 'warning')
 
1546
  return redirect(url_for('admin'))
 
1547
  except Exception as e:
1548
  logging.error(f"Error processing admin action '{action}': {e}", exc_info=True)
1549
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
1550
  return redirect(url_for('admin'))
 
 
1551
  current_data = load_data()
1552
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
1553
  display_categories = sorted(current_data.get('categories', []))
 
1554
  return render_template_string(
1555
  ADMIN_TEMPLATE,
1556
  products=display_products,
 
1558
  repo_id=REPO_ID,
1559
  currency_code=CURRENCY_CODE
1560
  )
 
1561
  @app.route('/force_upload', methods=['POST'])
1562
  def force_upload():
1563
  logging.info("Forcing upload to Hugging Face...")
 
1568
  logging.error(f"Error during forced upload: {e}", exc_info=True)
1569
  flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error')
1570
  return redirect(url_for('admin'))
 
1571
  @app.route('/force_download', methods=['POST'])
1572
  def force_download():
1573
  logging.info("Forcing download from Hugging Face...")
1574
  try:
1575
  if download_db_from_hf():
1576
  flash("Данные успешно скачаны с Hugging Face. Локальные файлы обновлены.", 'success')
1577
+ load_data()
1578
  else:
1579
  flash("Не удалось скачать данные с Hugging Face после нескольких попыток. Проверьте логи.", 'error')
1580
  except Exception as e:
1581
  logging.error(f"Error during forced download: {e}", exc_info=True)
1582
  flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
1583
  return redirect(url_for('admin'))
 
 
 
 
1584
  if __name__ == '__main__':
1585
  logging.info("Application starting up. Performing initial data load/download...")
1586
  download_db_from_hf()
1587
  load_data()
1588
  logging.info("Initial data load complete.")
 
1589
  if HF_TOKEN_WRITE:
1590
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1591
  backup_thread.start()
1592
  logging.info("Periodic backup thread started.")
1593
  else:
1594
  logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).")
 
1595
  port = int(os.environ.get('PORT', 7860))
1596
  logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
1597
  app.run(debug=False, host='0.0.0.0', port=port)