Kgshop commited on
Commit
778d272
·
verified ·
1 Parent(s): f0c25a4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +163 -160
app.py CHANGED
@@ -19,7 +19,6 @@ app = Flask(__name__)
19
  app.secret_key = 'dalarssi_secret_key_no_login'
20
  DATA_FILE = 'data.json'
21
 
22
-
23
  SYNC_FILES = [DATA_FILE]
24
 
25
  REPO_ID = "Kgshop/dallarsi"
@@ -173,7 +172,6 @@ translations = {
173
  }
174
  }
175
 
176
-
177
  def get_locale():
178
  return session.get('lang', 'ru')
179
 
@@ -190,7 +188,6 @@ def set_language(language):
190
  session['lang'] = language if language in translations else 'ru'
191
  return redirect(request.referrer or url_for('catalog'))
192
 
193
-
194
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
195
  if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
196
  logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
@@ -290,8 +287,6 @@ def periodic_backup():
290
  upload_db_to_hf()
291
  logging.info("Periodic backup finished.")
292
 
293
-
294
-
295
  def load_data():
296
  default_data = {'products': [], 'categories': [], 'orders': {}, 'employees': []}
297
  try:
@@ -359,8 +354,6 @@ def save_data(data):
359
  except Exception as e:
360
  logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
361
 
362
-
363
-
364
  CATALOG_TEMPLATE = '''
365
  <!DOCTYPE html>
366
  <html lang="{{ lang }}">
@@ -1285,7 +1278,6 @@ ADMIN_TEMPLATE = '''
1285
  <a href="{{ url_for('catalog') }}" class="button" style="background-color: #6b8e23;"><i class="fas fa-store"></i> Перейти в каталог</a>
1286
  </div>
1287
 
1288
-
1289
  {% with messages = get_flashed_messages(with_categories=true) %}
1290
  {% if messages %}
1291
  {% for category, message in messages %}
@@ -1323,23 +1315,27 @@ ADMIN_TEMPLATE = '''
1323
  </div>
1324
  </details>
1325
 
1326
- <h3>Существующие категории:</h3>
1327
- {% if categories %}
1328
- <div class="item-list">
1329
- {% for category in categories %}
1330
- <div class="item" style="display: flex; justify-content: space-between; align-items: center;">
1331
- <span>{{ category }}</span>
1332
- <form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить категорию \'{{ category }}\'? Товары этой категории будут помечены как \'Без категории\'.');">
1333
- <input type="hidden" name="action" value="delete_category">
1334
- <input type="hidden" name="category_name" value="{{ category }}">
1335
- <button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
1336
- </form>
1337
- </div>
1338
- {% endfor %}
1339
- </div>
1340
- {% else %}
1341
- <p>Категорий пока нет.</p>
1342
- {% endif %}
 
 
 
 
1343
  </div>
1344
  </div>
1345
 
@@ -1358,23 +1354,27 @@ ADMIN_TEMPLATE = '''
1358
  </div>
1359
  </details>
1360
 
1361
- <h3>Список сотруднико��:</h3>
1362
- {% if employees %}
1363
- <div class="item-list">
1364
- {% for employee in employees %}
1365
- <div class="item" style="display: flex; justify-content: space-between; align-items: center;">
1366
- <span>{{ employee }}</span>
1367
- <form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить сотрудника \'{{ employee }}\'?');">
1368
- <input type="hidden" name="action" value="delete_employee">
1369
- <input type="hidden" name="employee_name" value="{{ employee }}">
1370
- <button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
1371
- </form>
1372
- </div>
1373
- {% endfor %}
1374
- </div>
1375
- {% else %}
1376
- <p>Сотрудников пока нет.</p>
1377
- {% endif %}
 
 
 
 
1378
  </div>
1379
  </div>
1380
  </div>
@@ -1430,125 +1430,128 @@ ADMIN_TEMPLATE = '''
1430
  </div>
1431
  </details>
1432
 
1433
- <h3>Список товаров:</h3>
1434
- {% if products %}
1435
- <div class="item-list" id="admin-product-list">
1436
- {% for product in products %}
1437
- <div class="item" data-product-name="{{ product.name|lower }}">
1438
- <div style="display: flex; gap: 15px; align-items: flex-start;">
1439
- <div class="photo-preview" style="flex-shrink: 0;">
1440
- {% if product.get('photos') %}
1441
- <a href="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" target="_blank" title="Посмотреть первое фото">
1442
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" alt="Фото">
1443
- </a>
1444
- {% else %}
1445
- <img src="https://via.placeholder.com/70x70.png?text=N/A" alt="Нет фото">
1446
- {% endif %}
1447
- </div>
1448
- <div style="flex-grow: 1;">
1449
- <h3 style="margin-top: 0; margin-bottom: 5px; color: #3d3522;">
1450
- {{ product['name'] }}
1451
- {% if product.get('in_stock', True) %}
1452
- <span class="status-indicator in-stock">В наличии</span>
1453
- {% else %}
1454
- <span class="status-indicator out-of-stock">Нет в наличии</span>
1455
- {% endif %}
1456
- {% if product.get('is_top', False) %}
1457
- <span class="status-indicator top-product"><i class="fas fa-star"></i> Топ</span>
1458
- {% endif %}
1459
- </h3>
1460
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1461
- <p><strong>Цена за линейку:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
1462
- <p><strong>Шт. в линейке:</strong> {{ product.get('items_per_line', 'N/A') }}</p>
1463
- <p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
1464
- {% set colors = product.get('colors', []) %}
1465
- <p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
1466
- {% if product.get('photos') and product['photos']|length > 1 %}
1467
- <p style="font-size: 0.8rem; color: #a0aec0;">(Всего фото: {{ product['photos']|length }})</p>
1468
- {% endif %}
1469
- </div>
1470
- </div>
1471
-
1472
- <div class="item-actions">
1473
- <button type="button" class="button" onclick="toggleEditForm('edit-form-{{ product.id }}')"><i class="fas fa-edit"></i> Редактировать</button>
1474
- <form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить товар \'{{ product['name'] }}\'?');">
1475
- <input type="hidden" name="action" value="delete_product">
1476
- <input type="hidden" name="product_id" value="{{ product.id }}">
1477
- <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
1478
- </form>
1479
- </div>
1480
-
1481
- <div id="edit-form-{{ product.id }}" class="edit-form-container">
1482
- <h4><i class="fas fa-edit"></i> Редактирование: {{ product['name'] }}</h4>
1483
- <form method="POST" enctype="multipart/form-data">
1484
- <input type="hidden" name="action" value="edit_product">
1485
- <input type="hidden" name="product_id" value="{{ product.id }}">
1486
- <label>Название *:</label>
1487
- <input type="text" name="name" value="{{ product['name'] }}" required>
1488
- <label>Цена за линейку ({{ currency_code }}) *:</label>
1489
- <input type="number" name="price" step="0.01" min="0" value="{{ product['price'] }}" required>
1490
- <label>Штук в линейке *:</label>
1491
- <input type="number" name="items_per_line" min="1" step="1" value="{{ product.get('items_per_line', 1) }}" required>
1492
- <label>Описание:</label>
1493
- <textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
1494
- <label>Категория:</label>
1495
- <select name="category">
1496
- <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1497
- {% for category in categories %}
1498
- <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
1499
- {% endfor %}
1500
- </select>
1501
- <label>Заменить фотографии (выберите новые файлы, до 10 шт.):</label>
1502
- <input type="file" name="photos" accept="image/*" multiple>
1503
- {% if product.get('photos') %}
1504
- <p style="font-size: 0.85rem; margin-top: 5px;">Текущие фото:</p>
1505
- <div class="photo-preview">
1506
- {% for photo in product['photos'] %}
1507
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}" alt="Фото {{ loop.index }}">
1508
- {% endfor %}
1509
  </div>
1510
- {% endif %}
1511
- <label>Цвета/Варианты:</label>
1512
- <div id="edit-color-inputs-{{ product.id }}">
1513
- {% set current_colors = product.get('colors', []) %}
1514
- {% if current_colors and current_colors|select('ne', '')|list|length > 0 %}
1515
- {% for color in current_colors %}
1516
- {% if color.strip() %}
1517
- <div class="color-input-group">
1518
- <input type="text" name="colors" value="{{ color }}">
1519
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1520
- </div>
1521
- {% endif %}
1522
- {% endfor %}
1523
- {% else %}
1524
- <div class="color-input-group">
1525
- <input type="text" name="colors" placeholder="Например: Койот">
1526
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1527
- </div>
1528
- {% endif %}
1529
  </div>
1530
- <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('edit-color-inputs-{{ product.id }}')"><i class="fas fa-palette"></i> Добавить поле для цвета</button>
1531
- <br>
1532
- <div style="margin-top: 15px;">
1533
- <input type="checkbox" id="edit_in_stock_{{ product.id }}" name="in_stock" {% if product.get('in_stock', True) %}checked{% endif %}>
1534
- <label for="edit_in_stock_{{ product.id }}" class="inline-label">В наличии</label>
 
 
 
1535
  </div>
1536
- <div style="margin-top: 5px;">
1537
- <input type="checkbox" id="edit_is_top_{{ product.id }}" name="is_top" {% if product.get('is_top', False) %}checked{% endif %}>
1538
- <label for="edit_is_top_{{ product.id }}" class="inline-label">Топ товар</label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1539
  </div>
1540
- <br>
1541
- <button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> Сохранить изменения</button>
1542
- </form>
1543
  </div>
 
 
 
1544
  </div>
1545
- {% endfor %}
1546
- </div>
1547
- {% else %}
1548
- <p>Товаров пока нет.</p>
1549
- {% endif %}
1550
  </div>
1551
-
1552
  </div>
1553
 
1554
  <script>
@@ -1593,6 +1596,12 @@ ADMIN_TEMPLATE = '''
1593
  function filterAdminProducts() {
1594
  const searchTerm = document.getElementById('admin-product-search').value.toLowerCase().trim();
1595
  const productItems = document.querySelectorAll('#admin-product-list .item');
 
 
 
 
 
 
1596
  let visibleCount = 0;
1597
  productItems.forEach(item => {
1598
  const productName = item.getAttribute('data-product-name');
@@ -1609,8 +1618,6 @@ ADMIN_TEMPLATE = '''
1609
  </html>
1610
  '''
1611
 
1612
-
1613
-
1614
  @app.route('/')
1615
  def catalog():
1616
  data = load_data()
@@ -1891,7 +1898,6 @@ def admin():
1891
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
1892
  flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
1893
 
1894
-
1895
  new_product = {
1896
  'id': str(uuid.uuid4()),
1897
  'name': name, 'price': price, 'description': description,
@@ -2037,7 +2043,6 @@ def admin():
2037
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'.", 'error')
2038
  return redirect(url_for('admin'))
2039
 
2040
-
2041
  current_data = load_data()
2042
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
2043
  display_categories = sorted(current_data.get('categories', []))
@@ -2077,8 +2082,6 @@ def force_download():
2077
  flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
2078
  return redirect(url_for('admin'))
2079
 
2080
-
2081
-
2082
  if __name__ == '__main__':
2083
  logging.info("Application starting up. Performing initial data load/download...")
2084
  download_db_from_hf()
 
19
  app.secret_key = 'dalarssi_secret_key_no_login'
20
  DATA_FILE = 'data.json'
21
 
 
22
  SYNC_FILES = [DATA_FILE]
23
 
24
  REPO_ID = "Kgshop/dallarsi"
 
172
  }
173
  }
174
 
 
175
  def get_locale():
176
  return session.get('lang', 'ru')
177
 
 
188
  session['lang'] = language if language in translations else 'ru'
189
  return redirect(request.referrer or url_for('catalog'))
190
 
 
191
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
192
  if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
193
  logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
 
287
  upload_db_to_hf()
288
  logging.info("Periodic backup finished.")
289
 
 
 
290
  def load_data():
291
  default_data = {'products': [], 'categories': [], 'orders': {}, 'employees': []}
292
  try:
 
354
  except Exception as e:
355
  logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
356
 
 
 
357
  CATALOG_TEMPLATE = '''
358
  <!DOCTYPE html>
359
  <html lang="{{ lang }}">
 
1278
  <a href="{{ url_for('catalog') }}" class="button" style="background-color: #6b8e23;"><i class="fas fa-store"></i> Перейти в каталог</a>
1279
  </div>
1280
 
 
1281
  {% with messages = get_flashed_messages(with_categories=true) %}
1282
  {% if messages %}
1283
  {% for category, message in messages %}
 
1315
  </div>
1316
  </details>
1317
 
1318
+ <details>
1319
+ <summary><i class="fas fa-list"></i> Список категорий</summary>
1320
+ <div class="form-content">
1321
+ {% if categories %}
1322
+ <div class="item-list">
1323
+ {% for category in categories %}
1324
+ <div class="item" style="display: flex; justify-content: space-between; align-items: center;">
1325
+ <span>{{ category }}</span>
1326
+ <form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить категорию \'{{ category }}\'? Товары этой категории будут помечены как \'Без категории\'.');">
1327
+ <input type="hidden" name="action" value="delete_category">
1328
+ <input type="hidden" name="category_name" value="{{ category }}">
1329
+ <button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
1330
+ </form>
1331
+ </div>
1332
+ {% endfor %}
1333
+ </div>
1334
+ {% else %}
1335
+ <p>Категорий пока нет.</p>
1336
+ {% endif %}
1337
+ </div>
1338
+ </details>
1339
  </div>
1340
  </div>
1341
 
 
1354
  </div>
1355
  </details>
1356
 
1357
+ <details>
1358
+ <summary><i class="fas fa-list"></i> Список сотрудников</summary>
1359
+ <div class="form-content">
1360
+ {% if employees %}
1361
+ <div class="item-list">
1362
+ {% for employee in employees %}
1363
+ <div class="item" style="display: flex; justify-content: space-between; align-items: center;">
1364
+ <span>{{ employee }}</span>
1365
+ <form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить сотрудника \'{{ employee }}\'?');">
1366
+ <input type="hidden" name="action" value="delete_employee">
1367
+ <input type="hidden" name="employee_name" value="{{ employee }}">
1368
+ <button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
1369
+ </form>
1370
+ </div>
1371
+ {% endfor %}
1372
+ </div>
1373
+ {% else %}
1374
+ <p>Сотрудников пока нет.</p>
1375
+ {% endif %}
1376
+ </div>
1377
+ </details>
1378
  </div>
1379
  </div>
1380
  </div>
 
1430
  </div>
1431
  </details>
1432
 
1433
+ <details id="products-list-details">
1434
+ <summary><i class="fas fa-list"></i> Список товаров</summary>
1435
+ <div class="form-content">
1436
+ {% if products %}
1437
+ <div class="item-list" id="admin-product-list">
1438
+ {% for product in products %}
1439
+ <div class="item" data-product-name="{{ product.name|lower }}">
1440
+ <div style="display: flex; gap: 15px; align-items: flex-start;">
1441
+ <div class="photo-preview" style="flex-shrink: 0;">
1442
+ {% if product.get('photos') %}
1443
+ <a href="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" target="_blank" title="Посмотреть первое фото">
1444
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" alt="Фото">
1445
+ </a>
1446
+ {% else %}
1447
+ <img src="https://via.placeholder.com/70x70.png?text=N/A" alt="Нет фото">
1448
+ {% endif %}
1449
+ </div>
1450
+ <div style="flex-grow: 1;">
1451
+ <h3 style="margin-top: 0; margin-bottom: 5px; color: #3d3522;">
1452
+ {{ product['name'] }}
1453
+ {% if product.get('in_stock', True) %}
1454
+ <span class="status-indicator in-stock">В наличии</span>
1455
+ {% else %}
1456
+ <span class="status-indicator out-of-stock">Нет в наличии</span>
1457
+ {% endif %}
1458
+ {% if product.get('is_top', False) %}
1459
+ <span class="status-indicator top-product"><i class="fas fa-star"></i> Топ</span>
1460
+ {% endif %}
1461
+ </h3>
1462
+ <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1463
+ <p><strong>Цена за линейку:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
1464
+ <p><strong>Шт. в линейке:</strong> {{ product.get('items_per_line', 'N/A') }}</p>
1465
+ <p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
1466
+ {% set colors = product.get('colors', []) %}
1467
+ <p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
1468
+ {% if product.get('photos') and product['photos']|length > 1 %}
1469
+ <p style="font-size: 0.8rem; color: #a0aec0;">(Всего фото: {{ product['photos']|length }})</p>
1470
+ {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1471
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1472
  </div>
1473
+
1474
+ <div class="item-actions">
1475
+ <button type="button" class="button" onclick="toggleEditForm('edit-form-{{ product.id }}')"><i class="fas fa-edit"></i> Редактировать</button>
1476
+ <form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить товар \'{{ product['name'] }}\'?');">
1477
+ <input type="hidden" name="action" value="delete_product">
1478
+ <input type="hidden" name="product_id" value="{{ product.id }}">
1479
+ <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
1480
+ </form>
1481
  </div>
1482
+
1483
+ <div id="edit-form-{{ product.id }}" class="edit-form-container">
1484
+ <h4><i class="fas fa-edit"></i> Редактирование: {{ product['name'] }}</h4>
1485
+ <form method="POST" enctype="multipart/form-data">
1486
+ <input type="hidden" name="action" value="edit_product">
1487
+ <input type="hidden" name="product_id" value="{{ product.id }}">
1488
+ <label>Название *:</label>
1489
+ <input type="text" name="name" value="{{ product['name'] }}" required>
1490
+ <label>Цена за линейку ({{ currency_code }}) *:</label>
1491
+ <input type="number" name="price" step="0.01" min="0" value="{{ product['price'] }}" required>
1492
+ <label>Штук в линейке *:</label>
1493
+ <input type="number" name="items_per_line" min="1" step="1" value="{{ product.get('items_per_line', 1) }}" required>
1494
+ <label>Описание:</label>
1495
+ <textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
1496
+ <label>Категория:</label>
1497
+ <select name="category">
1498
+ <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1499
+ {% for category in categories %}
1500
+ <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
1501
+ {% endfor %}
1502
+ </select>
1503
+ <label>Заменить фотографии (выберите новые файлы, до 10 шт.):</label>
1504
+ <input type="file" name="photos" accept="image/*" multiple>
1505
+ {% if product.get('photos') %}
1506
+ <p style="font-size: 0.85rem; margin-top: 5px;">Текущие фото:</p>
1507
+ <div class="photo-preview">
1508
+ {% for photo in product['photos'] %}
1509
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}" alt="Фото {{ loop.index }}">
1510
+ {% endfor %}
1511
+ </div>
1512
+ {% endif %}
1513
+ <label>Цвета/Варианты:</label>
1514
+ <div id="edit-color-inputs-{{ product.id }}">
1515
+ {% set current_colors = product.get('colors', []) %}
1516
+ {% if current_colors and current_colors|select('ne', '')|list|length > 0 %}
1517
+ {% for color in current_colors %}
1518
+ {% if color.strip() %}
1519
+ <div class="color-input-group">
1520
+ <input type="text" name="colors" value="{{ color }}">
1521
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1522
+ </div>
1523
+ {% endif %}
1524
+ {% endfor %}
1525
+ {% else %}
1526
+ <div class="color-input-group">
1527
+ <input type="text" name="colors" placeholder="Например: Койот">
1528
+ <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1529
+ </div>
1530
+ {% endif %}
1531
+ </div>
1532
+ <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('edit-color-inputs-{{ product.id }}')"><i class="fas fa-palette"></i> Добавить поле для цвета</button>
1533
+ <br>
1534
+ <div style="margin-top: 15px;">
1535
+ <input type="checkbox" id="edit_in_stock_{{ product.id }}" name="in_stock" {% if product.get('in_stock', True) %}checked{% endif %}>
1536
+ <label for="edit_in_stock_{{ product.id }}" class="inline-label">В наличии</label>
1537
+ </div>
1538
+ <div style="margin-top: 5px;">
1539
+ <input type="checkbox" id="edit_is_top_{{ product.id }}" name="is_top" {% if product.get('is_top', False) %}checked{% endif %}>
1540
+ <label for="edit_is_top_{{ product.id }}" class="inline-label">Топ товар</label>
1541
+ </div>
1542
+ <br>
1543
+ <button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> Сохранить изменения</button>
1544
+ </form>
1545
  </div>
1546
+ </div>
1547
+ {% endfor %}
 
1548
  </div>
1549
+ {% else %}
1550
+ <p>Товаров пока нет.</p>
1551
+ {% endif %}
1552
  </div>
1553
+ </details>
 
 
 
 
1554
  </div>
 
1555
  </div>
1556
 
1557
  <script>
 
1596
  function filterAdminProducts() {
1597
  const searchTerm = document.getElementById('admin-product-search').value.toLowerCase().trim();
1598
  const productItems = document.querySelectorAll('#admin-product-list .item');
1599
+ const detailsTag = document.getElementById('products-list-details');
1600
+
1601
+ if (searchTerm.length > 0 && detailsTag && !detailsTag.hasAttribute('open')) {
1602
+ detailsTag.setAttribute('open', '');
1603
+ }
1604
+
1605
  let visibleCount = 0;
1606
  productItems.forEach(item => {
1607
  const productName = item.getAttribute('data-product-name');
 
1618
  </html>
1619
  '''
1620
 
 
 
1621
  @app.route('/')
1622
  def catalog():
1623
  data = load_data()
 
1898
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
1899
  flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
1900
 
 
1901
  new_product = {
1902
  'id': str(uuid.uuid4()),
1903
  'name': name, 'price': price, 'description': description,
 
2043
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'.", 'error')
2044
  return redirect(url_for('admin'))
2045
 
 
2046
  current_data = load_data()
2047
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
2048
  display_categories = sorted(current_data.get('categories', []))
 
2082
  flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
2083
  return redirect(url_for('admin'))
2084
 
 
 
2085
  if __name__ == '__main__':
2086
  logging.info("Application starting up. Performing initial data load/download...")
2087
  download_db_from_hf()