Shveiauto commited on
Commit
63f993a
·
verified ·
1 Parent(s): e8b19d5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +144 -35
app.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  from flask import Flask, render_template_string, request, redirect, url_for, session, send_file, flash
2
  import json
3
  import os
@@ -206,10 +208,13 @@ def periodic_backup():
206
  @app.route('/')
207
  def catalog():
208
  data = load_data()
209
- products = data.get('products', [])
210
  categories = data.get('categories', [])
211
  is_authenticated = 'user' in session
212
 
 
 
 
213
  catalog_html = '''
214
  <!DOCTYPE html>
215
  <html lang="ru">
@@ -252,8 +257,7 @@ def catalog():
252
  .category-filter.active, .category-filter:hover { background-color: #1C6758; color: white; border-color: #1C6758; box-shadow: 0 2px 10px rgba(28, 103, 88, 0.3); }
253
  body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { background-color: #3D8361; border-color: #3D8361; color: #1a2b26; box-shadow: 0 2px 10px rgba(61, 131, 97, 0.4); }
254
 
255
- .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; padding: 10px; }
256
- @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } }
257
  .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 #e1f0e9;}
258
  body.dark-mode .product { background: #253f37; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); border-color: #2c4a41; }
259
  .product:hover { transform: translateY(-5px) scale(1.02); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); }
@@ -316,6 +320,9 @@ def catalog():
316
  .notification.show { opacity: 1;}
317
  .no-results-message { grid-column: 1 / -1; text-align: center; padding: 40px; font-size: 1.1rem; color: #5e6e68; }
318
  body.dark-mode .no-results-message { color: #8aa39a; }
 
 
 
319
  </style>
320
  </head>
321
  <body>
@@ -354,6 +361,9 @@ def catalog():
354
  data-name="{{ product['name']|lower }}"
355
  data-description="{{ product.get('description', '')|lower }}"
356
  data-category="{{ product.get('category', 'Без категории') }}">
 
 
 
357
  <div class="product-image">
358
  {% if product.get('photos') and product['photos']|length > 0 %}
359
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
@@ -439,6 +449,7 @@ def catalog():
439
  const repoId = '{{ repo_id }}';
440
  const currencyCode = '{{ currency_code }}';
441
  const isAuthenticated = {{ is_authenticated|tojson }};
 
442
  let selectedProductIndex = null;
443
  let cart = JSON.parse(localStorage.getItem('soolaCart') || '[]');
444
 
@@ -703,28 +714,41 @@ def catalog():
703
  return;
704
  }
705
  let total = 0;
706
- let orderText = "Новый Заказ от Soola Cosmetics:%0A%0A";
 
 
 
707
  cart.forEach((item, index) => {
708
  const itemTotal = item.price * item.quantity;
709
  total += itemTotal;
710
- const colorText = item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
711
- orderText += `${index + 1}. ${item.name}${colorText} - ${item.price.toFixed(2)} ${currencyCode} × ${item.quantity} = ${itemTotal.toFixed(2)} ${currencyCode}%0A`;
 
 
 
712
  });
713
- orderText += `%0A*Итого: ${total.toFixed(2)} ${currencyCode}*%0A%0A`;
 
 
714
 
715
- const userInfo = {{ session.get('user_info', {})|tojson }};
716
  if (userInfo && userInfo.login) {
717
- orderText += `Заказчик: ${userInfo.first_name || ''} ${userInfo.last_name || ''}%0A`;
 
718
  orderText += `Логин: ${userInfo.login}%0A`;
 
 
 
719
  orderText += `Страна: ${userInfo.country || 'Не указана'}%0A`;
720
  orderText += `Город: ${userInfo.city || 'Не указан'}%0A`;
721
  } else {
722
- orderText += `Заказчик: (Не авторизован)%0A`;
723
  }
 
724
 
725
  const now = new Date();
726
- const dateTimeString = now.toLocaleString('ru-RU');
727
- orderText += `%0AДата заказа: ${dateTimeString}`;
 
728
 
729
  const whatsappNumber = "996997703090";
730
  const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${encodeURIComponent(orderText)}`;
@@ -757,11 +781,16 @@ def catalog():
757
  }
758
  });
759
 
760
- if (visibleProducts === 0 && (searchTerm || activeCategory !== 'all')) {
761
  const p = document.createElement('p');
762
  p.className = 'no-results-message';
763
  p.textContent = 'По вашему запросу товары не найдены.';
764
  grid.appendChild(p);
 
 
 
 
 
765
  }
766
  }
767
 
@@ -778,6 +807,7 @@ def catalog():
778
  filterProducts();
779
  });
780
  });
 
781
  }
782
 
783
  function showNotification(message, duration = 3000) {
@@ -824,7 +854,7 @@ def catalog():
824
  '''
825
  return render_template_string(
826
  catalog_html,
827
- products=products,
828
  categories=categories,
829
  repo_id=REPO_ID,
830
  is_authenticated=is_authenticated,
@@ -837,13 +867,18 @@ def catalog():
837
  @app.route('/product/<int:index>')
838
  def product_detail(index):
839
  data = load_data()
840
- products = data.get('products', [])
 
 
 
841
  is_authenticated = 'user' in session
842
  try:
843
- product = products[index]
 
 
844
  except IndexError:
845
- logging.warning(f"Попытка доступа к несуществующему продукту с индексом {index}")
846
- return "Товар не найден", 404
847
 
848
  detail_html = '''
849
  <div style="padding: 10px;">
@@ -955,7 +990,8 @@ def login():
955
  'first_name': user_info.get('first_name', ''),
956
  'last_name': user_info.get('last_name', ''),
957
  'country': user_info.get('country', ''),
958
- 'city': user_info.get('city', '')
 
959
  }
960
  logging.info(f"Пользователь {login} успешно вошел в систему.")
961
  login_response_html = f'''
@@ -997,7 +1033,8 @@ def auto_login():
997
  'first_name': user_info.get('first_name', ''),
998
  'last_name': user_info.get('last_name', ''),
999
  'country': user_info.get('country', ''),
1000
- 'city': user_info.get('city', '')
 
1001
  }
1002
  logging.info(f"Автоматический вход для пользователя {login} выполнен.")
1003
  return "OK", 200
@@ -1043,11 +1080,13 @@ ADMIN_TEMPLATE = '''
1043
  .section { margin-bottom: 30px; padding: 20px; background-color: #f8fcfb; border: 1px solid #d1e7dd; border-radius: 8px; }
1044
  form { margin-bottom: 20px; }
1045
  label { font-weight: 500; margin-top: 10px; display: block; color: #44524c; font-size: 0.9rem;}
1046
- input[type="text"], input[type="number"], input[type="password"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #c4d9d1; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; }
1047
  input:focus, textarea:focus, select:focus { border-color: #1C6758; outline: none; box-shadow: 0 0 0 2px rgba(28, 103, 88, 0.1); }
1048
  textarea { min-height: 80px; resize: vertical; }
1049
  input[type="file"] { padding: 8px; background-color: #f0f9f4; cursor: pointer; border: 1px solid #c4d9d1;}
1050
  input[type="file"]::file-selector-button { padding: 5px 10px; border-radius: 4px; background-color: #e0f0e9; border: 1px solid #c4d9d1; cursor: pointer; margin-right: 10px;}
 
 
1051
  button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #1C6758; 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;}
1052
  button:hover, .button:hover { background-color: #164B41; }
1053
  button:active, .button:active { transform: scale(0.98); }
@@ -1061,7 +1100,7 @@ ADMIN_TEMPLATE = '''
1061
  .item p { margin: 5px 0; font-size: 0.9rem; color: #44524c; }
1062
  .item strong { color: #2d332f; }
1063
  .item .description { font-size: 0.85rem; color: #5e6e68; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
1064
- .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
1065
  .item-actions button:not(.delete-button) { background-color: #1C6758; }
1066
  .item-actions button:not(.delete-button):hover { background-color: #164B41; }
1067
  .edit-form-container { margin-top: 15px; padding: 20px; background: #f0f9f4; border: 1px dashed #c4d9d1; border-radius: 6px; display: none; }
@@ -1087,6 +1126,10 @@ ADMIN_TEMPLATE = '''
1087
  .message.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb;}
1088
  .message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;}
1089
  .message.warning { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
 
 
 
 
1090
  </style>
1091
  </head>
1092
  <body>
@@ -1165,12 +1208,14 @@ ADMIN_TEMPLATE = '''
1165
  <label for="login">Логин *:</label>
1166
  <input type="text" id="login" name="login" required>
1167
  <label for="password">Пароль *:</label>
1168
- <input type="password" id="password" name="password" required title="Пароль будет сохранен в открытом вид��. Рекомендуется использовать менеджер паролей.">
1169
- <p style="font-size: 0.8rem; color: #777;">Логин и пароль обязательны,остальное,нет.</p>
1170
  <label for="first_name">Имя:</label>
1171
  <input type="text" id="first_name" name="first_name">
1172
  <label for="last_name">Фамилия:</label>
1173
  <input type="text" id="last_name" name="last_name">
 
 
1174
  <label for="country">Страна:</label>
1175
  <input type="text" id="country" name="country">
1176
  <label for="city">Город:</label>
@@ -1187,6 +1232,7 @@ ADMIN_TEMPLATE = '''
1187
  <div class="item">
1188
  <p><strong>Логин:</strong> {{ login }}</p>
1189
  <p><strong>Имя:</strong> {{ user_data.get('first_name', 'N/A') }} {{ user_data.get('last_name', '') }}</p>
 
1190
  <p><strong>Локация:</strong> {{ user_data.get('city', 'N/A') }}, {{ user_data.get('country', 'N/A') }}</p>
1191
  <div class="item-actions">
1192
  <form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить пользователя \'{{ login }}\'?');">
@@ -1237,6 +1283,15 @@ ADMIN_TEMPLATE = '''
1237
  </div>
1238
  <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('add-color-inputs')"><i class="fas fa-palette"></i> Добавить поле для цвета/варианта</button>
1239
  <br>
 
 
 
 
 
 
 
 
 
1240
  <button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> Добавить товар</button>
1241
  </form>
1242
  </div>
@@ -1258,7 +1313,17 @@ ADMIN_TEMPLATE = '''
1258
  {% endif %}
1259
  </div>
1260
  <div style="flex-grow: 1;">
1261
- <h3 style="margin-top: 0; margin-bottom: 5px; color: #2d332f;">{{ product['name'] }}</h3>
 
 
 
 
 
 
 
 
 
 
1262
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1263
  <p><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
1264
  <p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
@@ -1328,6 +1393,15 @@ ADMIN_TEMPLATE = '''
1328
  </div>
1329
  <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')"><i class="fas fa-palette"></i> Добавить поле для цвета</button>
1330
  <br>
 
 
 
 
 
 
 
 
 
1331
  <button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> Сохранить изменения</button>
1332
  </form>
1333
  </div>
@@ -1370,7 +1444,19 @@ ADMIN_TEMPLATE = '''
1370
  const group = button.closest('.color-input-group');
1371
  if (group) {
1372
  const container = group.parentNode;
 
 
1373
  group.remove();
 
 
 
 
 
 
 
 
 
 
1374
  } else {
1375
  console.warn("Не удалось найти родительский .color-input-group для кнопки удаления");
1376
  }
@@ -1431,6 +1517,9 @@ def admin():
1431
  category = request.form.get('category')
1432
  photos_files = request.files.getlist('photos')
1433
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
 
 
 
1434
 
1435
  if not name or not price_str:
1436
  flash("Название и цена товара обязательны.", 'error')
@@ -1489,10 +1578,11 @@ def admin():
1489
  new_product = {
1490
  'name': name, 'price': price, 'description': description,
1491
  'category': category if category in categories else 'Без категории',
1492
- 'photos': photos_list, 'colors': colors
 
1493
  }
1494
  products.append(new_product)
1495
- products.sort(key=lambda x: x.get('name', '').lower())
1496
  save_data(data)
1497
  logging.info(f"Товар '{name}' добавлен.")
1498
  flash(f"Товар '{name}' успешно добавлен.", 'success')
@@ -1505,9 +1595,13 @@ def admin():
1505
 
1506
  try:
1507
  index = int(index_str)
1508
- if not (0 <= index < len(products)): raise IndexError("Индекс вне диапазона")
1509
- product_to_edit = products[index]
 
 
 
1510
  original_name = product_to_edit.get('name', 'N/A')
 
1511
  except (ValueError, IndexError):
1512
  flash(f"Ошибка редактирования: неверный индекс товара '{index_str}'.", 'error')
1513
  return redirect(url_for('admin'))
@@ -1518,6 +1612,9 @@ def admin():
1518
  category = request.form.get('category')
1519
  product_to_edit['category'] = category if category in categories else 'Без категории'
1520
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
 
 
 
1521
 
1522
  try:
1523
  price = round(float(price_str), 2)
@@ -1586,7 +1683,7 @@ def admin():
1586
  elif uploaded_count == 0 and any(f.filename for f in photos_files):
1587
  flash("Не удалось загрузить новые фотографии.", "error")
1588
 
1589
- products.sort(key=lambda x: x.get('name', '').lower())
1590
  save_data(data)
1591
  logging.info(f"Товар '{original_name}' (индекс {index}) обновлен на '{product_to_edit['name']}'.")
1592
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
@@ -1599,8 +1696,9 @@ def admin():
1599
  return redirect(url_for('admin'))
1600
  try:
1601
  index = int(index_str)
1602
- if not (0 <= index < len(products)): raise IndexError("Индекс вне диапазона")
1603
- deleted_product = products.pop(index)
 
1604
  product_name = deleted_product.get('name', 'N/A')
1605
 
1606
  photos_to_delete = deleted_product.get('photos', [])
@@ -1632,6 +1730,7 @@ def admin():
1632
  password = request.form.get('password', '').strip()
1633
  first_name = request.form.get('first_name', '').strip()
1634
  last_name = request.form.get('last_name', '').strip()
 
1635
  country = request.form.get('country', '').strip()
1636
  city = request.form.get('city', '').strip()
1637
 
@@ -1645,6 +1744,7 @@ def admin():
1645
  users[login] = {
1646
  'password': password,
1647
  'first_name': first_name, 'last_name': last_name,
 
1648
  'country': country, 'city': city
1649
  }
1650
  save_users(users)
@@ -1673,13 +1773,21 @@ def admin():
1673
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
1674
  return redirect(url_for('admin'))
1675
 
1676
- products.sort(key=lambda x: x.get('name', '').lower())
 
 
 
 
 
 
 
 
1677
  categories.sort()
1678
  sorted_users = dict(sorted(users.items()))
1679
 
1680
  return render_template_string(
1681
  ADMIN_TEMPLATE,
1682
- products=products,
1683
  categories=categories,
1684
  users=sorted_users,
1685
  repo_id=REPO_ID,
@@ -1722,4 +1830,5 @@ if __name__ == '__main__':
1722
 
1723
  port = int(os.environ.get('PORT', 7860))
1724
  logging.info(f"Запуск Flask приложения на хосте 0.0.0.0 и порту {port}")
1725
- app.run(debug=False, host='0.0.0.0', port=port)
 
 
1
+
2
+
3
  from flask import Flask, render_template_string, request, redirect, url_for, session, send_file, flash
4
  import json
5
  import os
 
208
  @app.route('/')
209
  def catalog():
210
  data = load_data()
211
+ all_products = data.get('products', [])
212
  categories = data.get('categories', [])
213
  is_authenticated = 'user' in session
214
 
215
+ products_in_stock = [p for p in all_products if p.get('in_stock', True)]
216
+ products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
217
+
218
  catalog_html = '''
219
  <!DOCTYPE html>
220
  <html lang="ru">
 
257
  .category-filter.active, .category-filter:hover { background-color: #1C6758; color: white; border-color: #1C6758; box-shadow: 0 2px 10px rgba(28, 103, 88, 0.3); }
258
  body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { background-color: #3D8361; border-color: #3D8361; color: #1a2b26; box-shadow: 0 2px 10px rgba(61, 131, 97, 0.4); }
259
 
260
+ .products-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; padding: 10px; }
 
261
  .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 #e1f0e9;}
262
  body.dark-mode .product { background: #253f37; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); border-color: #2c4a41; }
263
  .product:hover { transform: translateY(-5px) scale(1.02); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); }
 
320
  .notification.show { opacity: 1;}
321
  .no-results-message { grid-column: 1 / -1; text-align: center; padding: 40px; font-size: 1.1rem; color: #5e6e68; }
322
  body.dark-mode .no-results-message { color: #8aa39a; }
323
+ .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); }
324
+ .product { position: relative; }
325
+
326
  </style>
327
  </head>
328
  <body>
 
361
  data-name="{{ product['name']|lower }}"
362
  data-description="{{ product.get('description', '')|lower }}"
363
  data-category="{{ product.get('category', 'Без категории') }}">
364
+ {% if product.get('is_top', False) %}
365
+ <span class="top-product-indicator"><i class="fas fa-star"></i> Топ</span>
366
+ {% endif %}
367
  <div class="product-image">
368
  {% if product.get('photos') and product['photos']|length > 0 %}
369
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
 
449
  const repoId = '{{ repo_id }}';
450
  const currencyCode = '{{ currency_code }}';
451
  const isAuthenticated = {{ is_authenticated|tojson }};
452
+ const userInfo = {{ session.get('user_info', {})|tojson }};
453
  let selectedProductIndex = null;
454
  let cart = JSON.parse(localStorage.getItem('soolaCart') || '[]');
455
 
 
714
  return;
715
  }
716
  let total = 0;
717
+ let orderText = "🛍️ *Новый Заказ от Soola Cosmetics* 🛍️%0A%0A";
718
+ orderText += "----------------------------------------%0A";
719
+ orderText += "*Детали заказа:*%0A";
720
+ orderText += "----------------------------------------%0A";
721
  cart.forEach((item, index) => {
722
  const itemTotal = item.price * item.quantity;
723
  total += itemTotal;
724
+ const colorText = item.color !== 'N/A' ? ` (${item.color})` : '';
725
+ orderText += `${index + 1}. *${item.name}*${colorText}%0A`;
726
+ orderText += ` Кол-во: ${item.quantity}%0A`;
727
+ orderText += ` Цена: ${item.price.toFixed(2)} ${currencyCode}%0A`;
728
+ orderText += ` *Сумма: ${itemTotal.toFixed(2)} ${currencyCode}*%0A%0A`;
729
  });
730
+ orderText += "----------------------------------------%0A";
731
+ orderText += `*ИТОГО: ${total.toFixed(2)} ${currencyCode}*%0A`;
732
+ orderText += "----------------------------------------%0A%0A";
733
 
 
734
  if (userInfo && userInfo.login) {
735
+ orderText += "*Данные клиента:*%0A";
736
+ orderText += `Имя: ${userInfo.first_name || ''} ${userInfo.last_name || ''}%0A`;
737
  orderText += `Логин: ${userInfo.login}%0A`;
738
+ if (userInfo.phone) {
739
+ orderText += `Телефон: ${userInfo.phone}%0A`;
740
+ }
741
  orderText += `Страна: ${userInfo.country || 'Не указана'}%0A`;
742
  orderText += `Город: ${userInfo.city || 'Не указан'}%0A`;
743
  } else {
744
+ orderText += "*Клиент не авторизован*%0A";
745
  }
746
+ orderText += "----------------------------------------%0A%0A";
747
 
748
  const now = new Date();
749
+ const dateTimeString = now.toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' });
750
+ orderText += `Дата заказа: ${dateTimeString}%0A`;
751
+ orderText += `_Сформировано автоматически_`;
752
 
753
  const whatsappNumber = "996997703090";
754
  const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${encodeURIComponent(orderText)}`;
 
781
  }
782
  });
783
 
784
+ if (visibleProducts === 0 && products.length > 0) {
785
  const p = document.createElement('p');
786
  p.className = 'no-results-message';
787
  p.textContent = 'По вашему запросу товары не найдены.';
788
  grid.appendChild(p);
789
+ } else if (products.length === 0 && !grid.querySelector('.no-results-message')) {
790
+ const p = document.createElement('p');
791
+ p.className = 'no-results-message';
792
+ p.textContent = 'Товары пока не добавлены.';
793
+ grid.appendChild(p);
794
  }
795
  }
796
 
 
807
  filterProducts();
808
  });
809
  });
810
+ filterProducts(); // Initial filter on load
811
  }
812
 
813
  function showNotification(message, duration = 3000) {
 
854
  '''
855
  return render_template_string(
856
  catalog_html,
857
+ products=products_sorted,
858
  categories=categories,
859
  repo_id=REPO_ID,
860
  is_authenticated=is_authenticated,
 
867
  @app.route('/product/<int:index>')
868
  def product_detail(index):
869
  data = load_data()
870
+ all_products = data.get('products', [])
871
+ products_in_stock = [p for p in all_products if p.get('in_stock', True)]
872
+ products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
873
+
874
  is_authenticated = 'user' in session
875
  try:
876
+ product = products_sorted[index]
877
+ if not product.get('in_stock', True):
878
+ raise IndexError("Товар не в наличии")
879
  except IndexError:
880
+ logging.warning(f"Попытка доступа к несуществующему или отсутствующему продукту с индексом {index}")
881
+ return "Товар не найден или отсутствует в наличии.", 404
882
 
883
  detail_html = '''
884
  <div style="padding: 10px;">
 
990
  'first_name': user_info.get('first_name', ''),
991
  'last_name': user_info.get('last_name', ''),
992
  'country': user_info.get('country', ''),
993
+ 'city': user_info.get('city', ''),
994
+ 'phone': user_info.get('phone', '')
995
  }
996
  logging.info(f"Пользователь {login} успешно вошел в систему.")
997
  login_response_html = f'''
 
1033
  'first_name': user_info.get('first_name', ''),
1034
  'last_name': user_info.get('last_name', ''),
1035
  'country': user_info.get('country', ''),
1036
+ 'city': user_info.get('city', ''),
1037
+ 'phone': user_info.get('phone', '')
1038
  }
1039
  logging.info(f"Автоматический вход для пользователя {login} выполнен.")
1040
  return "OK", 200
 
1080
  .section { margin-bottom: 30px; padding: 20px; background-color: #f8fcfb; border: 1px solid #d1e7dd; border-radius: 8px; }
1081
  form { margin-bottom: 20px; }
1082
  label { font-weight: 500; margin-top: 10px; display: block; color: #44524c; font-size: 0.9rem;}
1083
+ 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 #c4d9d1; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; }
1084
  input:focus, textarea:focus, select:focus { border-color: #1C6758; outline: none; box-shadow: 0 0 0 2px rgba(28, 103, 88, 0.1); }
1085
  textarea { min-height: 80px; resize: vertical; }
1086
  input[type="file"] { padding: 8px; background-color: #f0f9f4; cursor: pointer; border: 1px solid #c4d9d1;}
1087
  input[type="file"]::file-selector-button { padding: 5px 10px; border-radius: 4px; background-color: #e0f0e9; border: 1px solid #c4d9d1; cursor: pointer; margin-right: 10px;}
1088
+ input[type="checkbox"] { margin-right: 5px; vertical-align: middle; }
1089
+ label.inline-label { display: inline-block; margin-top: 10px; font-weight: normal; }
1090
  button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #1C6758; 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;}
1091
  button:hover, .button:hover { background-color: #164B41; }
1092
  button:active, .button:active { transform: scale(0.98); }
 
1100
  .item p { margin: 5px 0; font-size: 0.9rem; color: #44524c; }
1101
  .item strong { color: #2d332f; }
1102
  .item .description { font-size: 0.85rem; color: #5e6e68; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
1103
+ .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
1104
  .item-actions button:not(.delete-button) { background-color: #1C6758; }
1105
  .item-actions button:not(.delete-button):hover { background-color: #164B41; }
1106
  .edit-form-container { margin-top: 15px; padding: 20px; background: #f0f9f4; border: 1px dashed #c4d9d1; border-radius: 6px; display: none; }
 
1126
  .message.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb;}
1127
  .message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;}
1128
  .message.warning { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
1129
+ .status-indicator { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 0.8rem; font-weight: 500; margin-left: 10px; vertical-align: middle; }
1130
+ .status-indicator.in-stock { background-color: #c6f6d5; color: #2f855a; }
1131
+ .status-indicator.out-of-stock { background-color: #fed7d7; color: #c53030; }
1132
+ .status-indicator.top-product { background-color: #feebc8; color: #9c4221; margin-left: 5px;}
1133
  </style>
1134
  </head>
1135
  <body>
 
1208
  <label for="login">Логин *:</label>
1209
  <input type="text" id="login" name="login" required>
1210
  <label for="password">Пароль *:</label>
1211
+ <input type="password" id="password" name="password" required title="Пароль будет сохранен в открытом виде.">
1212
+ <p style="font-size: 0.8rem; color: #777;">Логин и пароль обязательны.</p>
1213
  <label for="first_name">Имя:</label>
1214
  <input type="text" id="first_name" name="first_name">
1215
  <label for="last_name">Фамилия:</label>
1216
  <input type="text" id="last_name" name="last_name">
1217
+ <label for="phone">Телефон:</label>
1218
+ <input type="tel" id="phone" name="phone">
1219
  <label for="country">Страна:</label>
1220
  <input type="text" id="country" name="country">
1221
  <label for="city">Город:</label>
 
1232
  <div class="item">
1233
  <p><strong>Логин:</strong> {{ login }}</p>
1234
  <p><strong>Имя:</strong> {{ user_data.get('first_name', 'N/A') }} {{ user_data.get('last_name', '') }}</p>
1235
+ <p><strong>Телефон:</strong> {{ user_data.get('phone', 'N/A') }}</p>
1236
  <p><strong>Локация:</strong> {{ user_data.get('city', 'N/A') }}, {{ user_data.get('country', 'N/A') }}</p>
1237
  <div class="item-actions">
1238
  <form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить пользователя \'{{ login }}\'?');">
 
1283
  </div>
1284
  <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('add-color-inputs')"><i class="fas fa-palette"></i> Добавить поле для цвета/варианта</button>
1285
  <br>
1286
+ <div style="margin-top: 15px;">
1287
+ <input type="checkbox" id="add_in_stock" name="in_stock" checked>
1288
+ <label for="add_in_stock" class="inline-label">В наличии</label>
1289
+ </div>
1290
+ <div style="margin-top: 5px;">
1291
+ <input type="checkbox" id="add_is_top" name="is_top">
1292
+ <label for="add_is_top" class="inline-label">Топ товар (показывать наверху)</label>
1293
+ </div>
1294
+ <br>
1295
  <button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> Добавить товар</button>
1296
  </form>
1297
  </div>
 
1313
  {% endif %}
1314
  </div>
1315
  <div style="flex-grow: 1;">
1316
+ <h3 style="margin-top: 0; margin-bottom: 5px; color: #2d332f;">
1317
+ {{ product['name'] }}
1318
+ {% if product.get('in_stock', True) %}
1319
+ <span class="status-indicator in-stock">В наличии</span>
1320
+ {% else %}
1321
+ <span class="status-indicator out-of-stock">Нет в наличии</span>
1322
+ {% endif %}
1323
+ {% if product.get('is_top', False) %}
1324
+ <span class="status-indicator top-product"><i class="fas fa-star"></i> Топ</span>
1325
+ {% endif %}
1326
+ </h3>
1327
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1328
  <p><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
1329
  <p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
 
1393
  </div>
1394
  <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')"><i class="fas fa-palette"></i> Добавить поле для цвета</button>
1395
  <br>
1396
+ <div style="margin-top: 15px;">
1397
+ <input type="checkbox" id="edit_in_stock_{{ loop.index0 }}" name="in_stock" {% if product.get('in_stock', True) %}checked{% endif %}>
1398
+ <label for="edit_in_stock_{{ loop.index0 }}" class="inline-label">В наличии</label>
1399
+ </div>
1400
+ <div style="margin-top: 5px;">
1401
+ <input type="checkbox" id="edit_is_top_{{ loop.index0 }}" name="is_top" {% if product.get('is_top', False) %}checked{% endif %}>
1402
+ <label for="edit_is_top_{{ loop.index0 }}" class="inline-label">Топ товар</label>
1403
+ </div>
1404
+ <br>
1405
  <button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> Сохранить изменения</button>
1406
  </form>
1407
  </div>
 
1444
  const group = button.closest('.color-input-group');
1445
  if (group) {
1446
  const container = group.parentNode;
1447
+ // Only remove if it's not the last one (or handle adding a placeholder if it is)
1448
+ // For simplicity, let's allow removing all. Add logic if needed later.
1449
  group.remove();
1450
+ // Optional: If container is now empty, add a placeholder input back
1451
+ if (container && container.children.length === 0) {
1452
+ const placeholderGroup = document.createElement('div');
1453
+ placeholderGroup.className = 'color-input-group';
1454
+ placeholderGroup.innerHTML = `
1455
+ <input type="text" name="colors" placeholder="Например: Цвет">
1456
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1457
+ `;
1458
+ container.appendChild(placeholderGroup);
1459
+ }
1460
  } else {
1461
  console.warn("Не удалось найти родительский .color-input-group для кнопки удаления");
1462
  }
 
1517
  category = request.form.get('category')
1518
  photos_files = request.files.getlist('photos')
1519
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1520
+ in_stock = 'in_stock' in request.form
1521
+ is_top = 'is_top' in request.form
1522
+
1523
 
1524
  if not name or not price_str:
1525
  flash("Название и цена товара обязательны.", 'error')
 
1578
  new_product = {
1579
  'name': name, 'price': price, 'description': description,
1580
  'category': category if category in categories else 'Без категории',
1581
+ 'photos': photos_list, 'colors': colors,
1582
+ 'in_stock': in_stock, 'is_top': is_top
1583
  }
1584
  products.append(new_product)
1585
+
1586
  save_data(data)
1587
  logging.info(f"Товар '{name}' добавлен.")
1588
  flash(f"Товар '{name}' успешно добавлен.", 'success')
 
1595
 
1596
  try:
1597
  index = int(index_str)
1598
+
1599
+ # We need to find the *original* index in the unsorted/unfiltered list
1600
+ original_product_list = data.get('products', [])
1601
+ if not (0 <= index < len(original_product_list)): raise IndexError("Индекс вне диапазона")
1602
+ product_to_edit = original_product_list[index]
1603
  original_name = product_to_edit.get('name', 'N/A')
1604
+
1605
  except (ValueError, IndexError):
1606
  flash(f"Ошибка редактирования: неверный индекс товара '{index_str}'.", 'error')
1607
  return redirect(url_for('admin'))
 
1612
  category = request.form.get('category')
1613
  product_to_edit['category'] = category if category in categories else 'Без категории'
1614
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1615
+ product_to_edit['in_stock'] = 'in_stock' in request.form
1616
+ product_to_edit['is_top'] = 'is_top' in request.form
1617
+
1618
 
1619
  try:
1620
  price = round(float(price_str), 2)
 
1683
  elif uploaded_count == 0 and any(f.filename for f in photos_files):
1684
  flash("Не удалось загрузить новые фотографии.", "error")
1685
 
1686
+
1687
  save_data(data)
1688
  logging.info(f"Товар '{original_name}' (индекс {index}) обновлен на '{product_to_edit['name']}'.")
1689
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
 
1696
  return redirect(url_for('admin'))
1697
  try:
1698
  index = int(index_str)
1699
+ original_product_list = data.get('products', [])
1700
+ if not (0 <= index < len(original_product_list)): raise IndexError("Индекс вне диапазона")
1701
+ deleted_product = original_product_list.pop(index)
1702
  product_name = deleted_product.get('name', 'N/A')
1703
 
1704
  photos_to_delete = deleted_product.get('photos', [])
 
1730
  password = request.form.get('password', '').strip()
1731
  first_name = request.form.get('first_name', '').strip()
1732
  last_name = request.form.get('last_name', '').strip()
1733
+ phone = request.form.get('phone', '').strip()
1734
  country = request.form.get('country', '').strip()
1735
  city = request.form.get('city', '').strip()
1736
 
 
1744
  users[login] = {
1745
  'password': password,
1746
  'first_name': first_name, 'last_name': last_name,
1747
+ 'phone': phone,
1748
  'country': country, 'city': city
1749
  }
1750
  save_users(users)
 
1773
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
1774
  return redirect(url_for('admin'))
1775
 
1776
+ # Pass the original, unsorted product list to the admin template with original indices
1777
+ original_products_with_indices = list(enumerate(data.get('products', [])))
1778
+ # Sort the indexed list for display purposes if needed, but keep original index
1779
+ display_products = sorted(original_products_with_indices, key=lambda item: item[1].get('name', '').lower())
1780
+ # Reconstruct list of products in display order for the template
1781
+ # Need to pass the original index within the product data or handle it carefully
1782
+ # Let's pass the original product list directly for simplicity in the template loops
1783
+ original_product_list = data.get('products', [])
1784
+
1785
  categories.sort()
1786
  sorted_users = dict(sorted(users.items()))
1787
 
1788
  return render_template_string(
1789
  ADMIN_TEMPLATE,
1790
+ products=original_product_list, # Pass the original list to preserve indices
1791
  categories=categories,
1792
  users=sorted_users,
1793
  repo_id=REPO_ID,
 
1830
 
1831
  port = int(os.environ.get('PORT', 7860))
1832
  logging.info(f"Запуск Flask приложения на хосте 0.0.0.0 и порту {port}")
1833
+ app.run(debug=False, host='0.0.0.0', port=port)
1834
+