Kgshop commited on
Commit
2a52994
·
verified ·
1 Parent(s): 702d490

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +459 -353
app.py CHANGED
@@ -11,7 +11,7 @@ from werkzeug.utils import secure_filename
11
  from werkzeug.security import generate_password_hash, check_password_hash
12
 
13
  app = Flask(__name__)
14
- app.secret_key = os.getenv("FLASK_SECRET_KEY", "your_default_secret_key") # для session
15
 
16
  DATA_FILE = 'data_exmenu.json'
17
  USER_DATA_FILE = 'data_emirusers.json'
@@ -21,8 +21,9 @@ REPO_ID = "Kgshop/clients"
21
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
22
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
23
  LOGO_URL = "https://huggingface.co/spaces/kgmenu/Example/resolve/main/emir_chaihana_14040103_125008071.jpg"
24
- ADMIN_LOGIN = "admin"
25
- ADMIN_PASSWORD = "admin"
 
26
  # Настройка логирования
27
  logging.basicConfig(level=logging.DEBUG)
28
 
@@ -249,6 +250,7 @@ def redeem_points_from_user(login, points_to_redeem):
249
  return False, "Недостаточно баллов для списания."
250
  return False, "Пользователь не найден."
251
 
 
252
  @app.route('/')
253
  def menu():
254
  data = load_data()
@@ -257,7 +259,6 @@ def menu():
257
  stoplist = data['stoplist']
258
  category_counts = get_category_counts(products)
259
 
260
- # Удаляем просроченные записи из стоп-листа
261
  current_time = datetime.now()
262
  for product_id, stop_info in list(stoplist.items()):
263
  if stop_info['until'] <= current_time:
@@ -706,13 +707,6 @@ def menu():
706
  text-align: center;
707
  margin: 5px 0;
708
  }
709
- .timer {
710
- color: #ef4444;
711
- font-weight: 500;
712
- display: inline-block;
713
- min-width: 60px; /* Чтобы текст не прыгал при обновлении */
714
- text-align: center;
715
- }
716
  .footer-info {
717
  text-align: center;
718
  margin-top: 20px;
@@ -777,6 +771,7 @@ def menu():
777
  .logout-button:hover {
778
  background-color: #dc2626;
779
  }
 
780
  </style>
781
  </head>
782
  <body>
@@ -821,7 +816,7 @@ def menu():
821
  <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
822
  <div id="stop-status-{{ loop.index0 }}">
823
  {% if stoplist[loop.index0|string] %}
824
- <p class="stop-notice" id="stop-timer-{{ loop.index0 }}">Извините, блюдо на стопе, будет готово через <span class="timer" data-until="{{ stoplist[loop.index0|string]['until'] }}"></span></p>
825
  {% else %}
826
  <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
827
  <button class="product-button add-to-cart" onclick="openOptionsModal({{ loop.index0 }})">В корзину</button>
@@ -977,6 +972,7 @@ def menu():
977
  </div>
978
  </div>
979
 
 
980
  <button id="cart-button" onclick="openCartModal()">🛒</button>
981
 
982
  <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
@@ -1149,6 +1145,7 @@ def menu():
1149
  }
1150
  }
1151
 
 
1152
  function orderViaWhatsApp() {
1153
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1154
  if (cart.length === 0) {
@@ -1195,6 +1192,7 @@ def menu():
1195
  window.open(`https://api.whatsapp.com/send?phone=+996500131380&text=${orderText}`, '_blank');
1196
  }
1197
 
 
1198
  function orderViaWhatsAppWithQR() {
1199
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1200
  if (cart.length === 0) {
@@ -1224,7 +1222,7 @@ def menu():
1224
  }
1225
  total -= redeemedPoints;
1226
  orderText += `%0AСписано баллов: ${redeemedPoints} с`;
1227
- $.post('/redeem_points', { points: redeemedPoints }, function(response) {
1228
  if (response.status === 'success') {
1229
  availablePoints -= redeemedPoints;
1230
  document.getElementById('availablePoints').textContent = availablePoints;
@@ -1294,17 +1292,14 @@ def menu():
1294
 
1295
  function startTimer(productId, until) {
1296
  const timerEl = document.querySelector(`#stop-timer-${productId} .timer`);
1297
- if (!timerEl) {
1298
- console.error(`Таймер для productId ${productId} не найден`);
1299
- return;
1300
- }
1301
 
1302
  function updateTimer() {
1303
  const remaining = new Date(until) - new Date();
1304
  if (remaining > 0) {
1305
  const minutes = Math.floor(remaining / 60000);
1306
  const seconds = Math.floor((remaining % 60000) / 1000);
1307
- timerEl.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
1308
  } else {
1309
  clearInterval(timerInterval);
1310
  const stopStatus = document.getElementById(`stop-status-${productId}`);
@@ -1313,23 +1308,6 @@ def menu():
1313
  <button class="product-button add-to-cart" onclick="openOptionsModal(${productId})">В корзину</button>
1314
  `;
1315
  delete stoplist[productId];
1316
- // Отправляем запрос на сервер для удаления из стоп-листа
1317
- $.ajax({
1318
- url: '/stoplist',
1319
- type: 'POST',
1320
- data: {
1321
- action: 'remove',
1322
- product_id: productId
1323
- },
1324
- success: function(response) {
1325
- if (response.status !== 'success') {
1326
- console.error('Ошибка при удалении из стоп-листа на сервере:', response);
1327
- }
1328
- },
1329
- error: function() {
1330
- console.error('Ошибка сервера при удалении из стоп-листа');
1331
- }
1332
- });
1333
  }
1334
  }
1335
  updateTimer();
@@ -1338,7 +1316,6 @@ def menu():
1338
 
1339
  // Запускаем таймеры только для активных стопов
1340
  Object.entries(stoplist).forEach(([id, stopInfo]) => {
1341
- console.log(`Запуск таймера для productId ${id} с until=${stopInfo.until}`);
1342
  startTimer(id, stopInfo.until);
1343
  });
1344
 
@@ -1412,6 +1389,8 @@ def menu():
1412
  }
1413
  });
1414
  });
 
 
1415
  </script>
1416
  </body>
1417
  </html>
@@ -1473,27 +1452,17 @@ def stoplist():
1473
  products = data['products']
1474
  stoplist = data['stoplist']
1475
 
1476
- # Удаляем просроченные записи из стоп-листа
1477
- current_time = datetime.now()
1478
- for product_id, stop_info in list(stoplist.items()):
1479
- if stop_info['until'] <= current_time:
1480
- del stoplist[product_id]
1481
- save_data(data)
1482
-
1483
  if request.method == 'POST':
1484
  action = request.form.get('action')
1485
  if action == 'add':
1486
  product_id = request.form.get('product_id')
1487
  minutes = int(request.form.get('minutes', 0))
1488
  if minutes > 0:
1489
- # Сохраняем datetime в переменной до вызова save_data
1490
- until_datetime = datetime.now() + timedelta(minutes=minutes)
1491
  stoplist[product_id] = {
1492
- 'until': until_datetime
1493
  }
1494
- save_data(data) # Это преобразует until в строку в stoplist
1495
- # Используем сохранённый until_datetime для ответа
1496
- return jsonify({'status': 'success', 'until': until_datetime.isoformat()})
1497
  return jsonify({'status': 'error', 'message': 'Invalid minutes'}), 400
1498
  elif action == 'remove':
1499
  product_id = request.form.get('product_id')
@@ -1503,6 +1472,7 @@ def stoplist():
1503
  return jsonify({'status': 'success'})
1504
  return jsonify({'status': 'error', 'message': 'Product not in stoplist'}), 404
1505
 
 
1506
  stoplist_for_template = {
1507
  k: {'until': v['until'].isoformat() if isinstance(v['until'], datetime) else v['until']}
1508
  for k, v in stoplist.items()
@@ -1568,9 +1538,6 @@ def stoplist():
1568
  .timer {
1569
  color: #ef4444;
1570
  font-weight: 500;
1571
- display: inline-block;
1572
- min-width: 60px; /* Чтобы текст не прыгал при обновлении */
1573
- text-align: center;
1574
  }
1575
  .stop-notice {
1576
  color: #ef4444;
@@ -1594,7 +1561,7 @@ def stoplist():
1594
  <h3>{{ product['name'] }}</h3>
1595
  <div class="stop-status" id="stop-status-{{ loop.index0 }}">
1596
  {% if stoplist[loop.index0|string] %}
1597
- <p class="stop-notice" id="stop-timer-{{ loop.index0 }}">Извините, блюдо на стопе, будет готово через <span class="timer" data-until="{{ stoplist[loop.index0|string]['until'] }}"></span></p>
1598
  {% else %}
1599
  <form class="stop-form" data-id="{{ loop.index0 }}">
1600
  <button type="button" onclick="addToStoplist({{ loop.index0 }}, 30)">30 мин</button>
@@ -1628,20 +1595,22 @@ def stoplist():
1628
  if (response.status === 'success') {
1629
  stoplist[productId] = { until: response.until };
1630
  const stopStatus = document.getElementById(`stop-status-${productId}`);
1631
- stopStatus.innerHTML = `<p class="stop-notice" id="stop-timer-${productId}">Извините, блюдо на стопе, будет готово через <span class="timer" data-until="${response.until}"></span></p>`;
1632
  let productItem = document.querySelector(`.product-item[data-id='${productId}']`);
1633
  let removeButton = document.createElement('button');
1634
  removeButton.className = 'remove-stop-button';
1635
  removeButton.textContent = 'Снять стоп';
1636
  removeButton.onclick = function() { removeFromStoplist(productId); };
1637
  productItem.appendChild(removeButton);
 
 
1638
  startTimer(productId, response.until);
1639
  } else {
1640
- alert('Ошибка при добавлении в стоп-лист: ' + response.message);
1641
  }
1642
  },
1643
  error: function() {
1644
- alert('Ошибка сервера при добавлении в стоп-лист');
1645
  }
1646
  });
1647
  }
@@ -1671,54 +1640,61 @@ def stoplist():
1671
  productItem.removeChild(removeButton);
1672
  }
1673
  } else {
1674
- alert('Ошибка при удалении из стоп-листа: ' + response.message);
1675
  }
1676
  },
1677
  error: function() {
1678
- alert('Ошибка сервера при удалении из стоп-листа');
1679
  }
1680
  });
1681
  }
1682
 
 
1683
  function startTimer(productId, until) {
1684
  const timerEl = document.querySelector(`#stop-timer-${productId} .timer`);
1685
- if (!timerEl) {
1686
- console.error(`Таймер для productId ${productId} не найден`);
1687
- return;
1688
- }
1689
 
1690
  function updateTimer() {
1691
  const remaining = new Date(until) - new Date();
1692
  if (remaining > 0) {
1693
  const minutes = Math.floor(remaining / 60000);
1694
  const seconds = Math.floor((remaining % 60000) / 1000);
1695
- timerEl.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
1696
  } else {
1697
  clearInterval(timerInterval);
1698
- removeFromStoplist(productId);
 
 
 
 
 
 
 
 
 
 
 
 
 
1699
  }
1700
  }
1701
  updateTimer();
1702
  const timerInterval = setInterval(updateTimer, 1000);
1703
  }
1704
 
1705
- // Запускаем таймеры для всех продуктов в стоп-листе
1706
  Object.entries(stoplist).forEach(([id, stopInfo]) => {
1707
- console.log(`Запуск таймера для productId ${id} с until=${stopInfo.until}`);
1708
  startTimer(id, stopInfo.until);
1709
  });
1710
  </script>
1711
  </body>
1712
  </html>
1713
  '''
1714
- return render_template_string(stoplist_html, products=products, stoplist=stoplist, stoplist_for_js=stoplist_for_js)
1715
 
1716
- # --- Admin Routes ---
1717
  @app.route('/admin', methods=['GET', 'POST'])
1718
  def admin():
1719
- if 'admin_login' not in session:
1720
- return redirect(url_for('admin_login'))
1721
-
1722
  data = load_data()
1723
  products = data['products']
1724
  categories = data['categories']
@@ -1732,22 +1708,22 @@ def admin():
1732
  if category_name and category_name not in categories:
1733
  categories.append(category_name)
1734
  save_data(data)
1735
- return redirect(url_for('admin'))
 
1736
 
1737
  elif action == 'delete_category':
1738
  category_index = int(request.form.get('category_index'))
1739
- category_to_delete = categories[category_index]
1740
- del categories[category_index]
1741
- # Обновляем категорию у продуктов
1742
  for product in products:
1743
- if product.get('category') == category_to_delete:
1744
  product['category'] = 'Без категории'
1745
  save_data(data)
1746
  return redirect(url_for('admin'))
1747
 
1748
  elif action == 'add':
 
1749
  name = request.form.get('name')
1750
- price = float(request.form.get('price').replace(',', '.'))
1751
  description = request.form.get('description')
1752
  category = request.form.get('category')
1753
  photos_files = request.files.getlist('photos')
@@ -1756,7 +1732,6 @@ def admin():
1756
  photos_list = []
1757
  options_list = []
1758
 
1759
- # Обрабатываем фотографии
1760
  if photos_files:
1761
  for photo in photos_files[:10]:
1762
  if photo and photo.filename:
@@ -1778,7 +1753,6 @@ def admin():
1778
  if os.path.exists(temp_path):
1779
  os.remove(temp_path)
1780
 
1781
- # Обрабатываем опции
1782
  for opt_name, opt_price in zip(option_names, option_prices):
1783
  if opt_name and opt_price:
1784
  options_list.append({
@@ -1786,6 +1760,10 @@ def admin():
1786
  'price': float(opt_price.replace(',', '.'))
1787
  })
1788
 
 
 
 
 
1789
  new_product = {
1790
  'name': name,
1791
  'price': price,
@@ -1799,19 +1777,18 @@ def admin():
1799
  return redirect(url_for('admin'))
1800
 
1801
  elif action == 'edit':
1802
- product_index = int(request.form.get('product_index'))
 
1803
  name = request.form.get('name')
1804
- price = float(request.form.get('price').replace(',', '.'))
1805
  description = request.form.get('description')
1806
  category = request.form.get('category')
1807
  photos_files = request.files.getlist('photos')
1808
  option_names = request.form.getlist('option_names')
1809
  option_prices = request.form.getlist('option_prices')
1810
- photos_list = request.form.getlist('existing_photos') # Существующие фото
1811
- options_list = []
1812
 
1813
- # Обрабатываем новые фотографии
1814
- if photos_files:
1815
  for photo in photos_files[:10]:
1816
  if photo and photo.filename:
1817
  photo_filename = secure_filename(photo.filename)
@@ -1826,13 +1803,14 @@ def admin():
1826
  repo_id=REPO_ID,
1827
  repo_type="dataset",
1828
  token=HF_TOKEN_WRITE,
1829
- commit_message=f"Добавлено фото для блюда {name}"
1830
  )
1831
- photos_list.append(photo_filename)
1832
  if os.path.exists(temp_path):
1833
  os.remove(temp_path)
 
1834
 
1835
- # Обрабатываем опции
1836
  for opt_name, opt_price in zip(option_names, option_prices):
1837
  if opt_name and opt_price:
1838
  options_list.append({
@@ -1840,38 +1818,24 @@ def admin():
1840
  'price': float(opt_price.replace(',', '.'))
1841
  })
1842
 
1843
- products[product_index].update({
1844
- 'name': name,
1845
- 'price': price,
1846
- 'description': description,
1847
- 'category': category if category in categories else 'Без категории',
1848
- 'photos': photos_list,
1849
- 'options': options_list
1850
- })
1851
  save_data(data)
1852
  return redirect(url_for('admin'))
1853
 
1854
  elif action == 'delete':
1855
- product_index = int(request.form.get('product_index'))
1856
- products.pop(product_index)
1857
- # Удаляем продукт из стоп-листа, если он там есть
1858
- product_id = str(product_index)
1859
- if product_id in stoplist:
1860
- del stoplist[product_id]
1861
- # Обновляем индексы в стоп-листе
1862
- new_stoplist = {}
1863
- for pid, stop_info in stoplist.items():
1864
- pid_int = int(pid)
1865
- if pid_int > product_index:
1866
- new_stoplist[str(pid_int - 1)] = stop_info
1867
- elif pid_int < product_index:
1868
- new_stoplist[pid] = stop_info
1869
- data['stoplist'] = new_stoplist
1870
  save_data(data)
1871
  return redirect(url_for('admin'))
1872
 
1873
- elif action == 'upload_qr':
1874
- qr_file = request.files.get('qr_file')
1875
  if qr_file and qr_file.filename:
1876
  qr_filename = secure_filename(qr_file.filename)
1877
  uploads_dir = 'uploads'
@@ -1885,13 +1849,18 @@ def admin():
1885
  repo_id=REPO_ID,
1886
  repo_type="dataset",
1887
  token=HF_TOKEN_WRITE,
1888
- commit_message="Добавлен QR-код для оплаты"
1889
  )
1890
  data['qr_code'] = qr_filename
1891
  save_data(data)
1892
  if os.path.exists(temp_path):
1893
  os.remove(temp_path)
1894
- return redirect(url_for('admin'))
 
 
 
 
 
1895
 
1896
  admin_html = '''
1897
  <!DOCTYPE html>
@@ -1912,98 +1881,159 @@ def admin():
1912
  max-width: 1200px;
1913
  margin: 0 auto;
1914
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1915
  h1, h2 {
1916
  font-weight: 600;
1917
  margin-bottom: 20px;
1918
  }
1919
- .form-section, .category-section, .product-section {
1920
  background: #fff;
1921
  padding: 20px;
1922
  border-radius: 15px;
1923
  box-shadow: 0 4px 15px rgba(0,0,0,0.1);
1924
- margin-bottom: 20px;
1925
  }
1926
  label {
 
 
1927
  display: block;
1928
- margin: 10px 0 5px;
1929
  }
1930
- input, select, textarea {
1931
  width: 100%;
1932
- padding: 10px;
1933
- margin-bottom: 10px;
1934
  border: 1px solid #e2e8f0;
1935
  border-radius: 8px;
1936
  font-size: 1rem;
 
 
 
 
 
 
1937
  }
1938
  button {
1939
- padding: 10px 20px;
1940
  border: none;
1941
  border-radius: 8px;
1942
  background-color: #3b82f6;
1943
  color: white;
 
1944
  cursor: pointer;
1945
- margin: 5px;
 
1946
  }
1947
  button:hover {
1948
  background-color: #2563eb;
 
 
1949
  }
1950
  .delete-button {
1951
  background-color: #ef4444;
1952
  }
1953
  .delete-button:hover {
1954
  background-color: #dc2626;
 
1955
  }
1956
- .category-item, .product-item {
1957
- display: flex;
1958
- justify-content: space-between;
1959
- align-items: center;
1960
- padding: 10px;
1961
- border-bottom: 1px solid #e2e8f0;
 
 
 
 
 
 
 
 
 
1962
  }
1963
- .option-field {
1964
  display: flex;
1965
  gap: 10px;
1966
- margin-bottom: 10px;
1967
  }
1968
- .option-field input {
1969
- flex: 1;
 
 
 
 
 
 
1970
  }
1971
  </style>
1972
  </head>
1973
  <body>
1974
  <div class="container">
1975
- <h1>Админ-панель</h1>
1976
-
1977
- <!-- Управление QR-кодом -->
1978
- <div class="form-section">
1979
- <h2>Управление QR-кодом для оплаты</h2>
1980
- <form method="POST" enctype="multipart/form-data">
1981
- <input type="hidden" name="action" value="upload_qr">
1982
- <label for="qr_file">Загрузить QR-код:</label>
1983
- <input type="file" name="qr_file" id="qr_file" accept="image/*">
1984
- <button type="submit">Загрузить</button>
1985
- </form>
1986
- {% if qr_code %}
1987
- <p>Текущий QR-код: <a href="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ qr_code }}" target="_blank">Просмотреть</a></p>
1988
- {% else %}
1989
- <p>QR-код не установлен</p>
1990
- {% endif %}
1991
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1992
 
1993
- <!-- Управление категориями -->
1994
- <div class="category-section">
1995
- <h2>Управление категориями</h2>
1996
- <form method="POST">
1997
- <input type="hidden" name="action" value="add_category">
1998
- <label for="category_name">Новая категория:</label>
1999
- <input type="text" name="category_name" id="category_name" required>
2000
- <button type="submit">Добавить категорию</button>
2001
- </form>
2002
- <h3>Существующие категории:</h3>
2003
  {% for category in categories %}
2004
  <div class="category-item">
2005
- <span>{{ category }}</span>
2006
- <form method="POST" style="display:inline;">
2007
  <input type="hidden" name="action" value="delete_category">
2008
  <input type="hidden" name="category_index" value="{{ loop.index0 }}">
2009
  <button type="submit" class="delete-button">Удалить</button>
@@ -2012,222 +2042,294 @@ def admin():
2012
  {% endfor %}
2013
  </div>
2014
 
2015
- <!-- Добавление блюда -->
2016
- <div class="form-section">
2017
- <h2>Добавить блюдо</h2>
2018
- <form method="POST" enctype="multipart/form-data">
2019
- <input type="hidden" name="action" value="add">
2020
- <label for="name">Название:</label>
2021
- <input type="text" name="name" id="name" required>
2022
- <label for="price">Цена:</label>
2023
- <input type="text" name="price" id="price" required>
2024
- <label for="description">Описание:</label>
2025
- <textarea name="description" id="description" required></textarea>
2026
- <label for="category">Категория:</label>
2027
- <select name="category" id="category">
2028
- <option value="Без категории">Без категории</option>
2029
- {% for category in categories %}
2030
- <option value="{{ category }}">{{ category }}</option>
2031
- {% endfor %}
2032
- </select>
2033
- <label for="photos">Фотографии:</label>
2034
- <input type="file" name="photos" id="photos" multiple accept="image/*">
2035
- <div id="options-container">
2036
- <h3>Дополнительные опции</h3>
2037
- <div class="option-field">
2038
- <input type="text" name="option_names" placeholder="Название опции">
2039
- <input type="text" name="option_prices" placeholder="Цена опции">
2040
- </div>
2041
- </div>
2042
- <button type="button" onclick="addOptionField()">Добавить опцию</button>
2043
- <button type="submit">Добавить блюдо</button>
2044
- </form>
2045
- </div>
2046
 
2047
- <!-- Список блюд -->
2048
- <div class="product-section">
2049
- <h2>Список блюд</h2>
2050
  {% for product in products %}
2051
  <div class="product-item">
2052
- <div>
2053
- <strong>{{ product['name'] }}</strong> - {{ product['price'] }} с ({{ product.get('category', 'Без категории') }})
2054
- </div>
2055
- <div>
2056
- <button onclick="editProduct({{ loop.index0 }})">Редактировать</button>
2057
- <form method="POST" style="display:inline;">
2058
- <input type="hidden" name="action" value="delete">
2059
- <input type="hidden" name="product_index" value="{{ loop.index0 }}">
2060
- <button type="submit" class="delete-button">Удалить</button>
2061
- </form>
2062
- </div>
2063
- </div>
2064
- <div id="edit-form-{{ loop.index0 }}" style="display:none;">
2065
- <form method="POST" enctype="multipart/form-data">
2066
- <input type="hidden" name="action" value="edit">
2067
- <input type="hidden" name="product_index" value="{{ loop.index0 }}">
2068
- <label for="name-{{ loop.index0 }}">Название:</label>
2069
- <input type="text" name="name" id="name-{{ loop.index0 }}" value="{{ product['name'] }}" required>
2070
- <label for="price-{{ loop.index0 }}">Цена:</label>
2071
- <input type="text" name="price" id="price-{{ loop.index0 }}" value="{{ product['price'] }}" required>
2072
- <label for="description-{{ loop.index0 }}">Описание:</label>
2073
- <textarea name="description" id="description-{{ loop.index0 }}" required>{{ product['description'] }}</textarea>
2074
- <label for="category-{{ loop.index0 }}">Категория:</label>
2075
- <select name="category" id="category-{{ loop.index0 }}">
2076
- <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
2077
- {% for category in categories %}
2078
- <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
2079
- {% endfor %}
2080
- </select>
2081
- <label>Текущие фотографии:</label>
2082
- {% for photo in product.get('photos', []) %}
2083
- <div>
2084
- <input type="checkbox" name="existing_photos" value="{{ photo }}" checked>
2085
- <a href="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}" target="_blank">{{ photo }}</a>
2086
- </div>
2087
  {% endfor %}
2088
- <label for="photos-{{ loop.index0 }}">Добавить новые фотографии:</label>
2089
- <input type="file" name="photos" id="photos-{{ loop.index0 }}" multiple accept="image/*">
2090
- <div id="edit-options-container-{{ loop.index0 }}">
2091
- <h3>Дополнительные опции</h3>
2092
- {% for option in product.get('options', []) %}
2093
- <div class="option-field">
2094
- <input type="text" name="option_names" value="{{ option['name'] }}" placeholder="Название опции">
2095
- <input type="text" name="option_prices" value="{{ option['price'] }}" placeholder="Цена опции">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2096
  </div>
2097
- {% endfor %}
2098
- </div>
2099
- <button type="button" onclick="addEditOptionField({{ loop.index0 }})">Добавить опцию</button>
2100
- <button type="submit">Сохранить изменения</button>
 
 
 
 
2101
  </form>
2102
  </div>
2103
  {% endfor %}
2104
  </div>
2105
  </div>
2106
  <script>
2107
- function addOptionField() {
2108
- const container = document.getElementById('options-container');
2109
- const newField = document.createElement('div');
2110
- newField.className = 'option-field';
2111
- newField.innerHTML = `
2112
  <input type="text" name="option_names" placeholder="Название опции">
2113
- <input type="text" name="option_prices" placeholder="Цена опции">
2114
  `;
2115
- container.appendChild(newField);
2116
  }
2117
 
2118
- function addEditOptionField(index) {
2119
- const container = document.getElementById(`edit-options-container-${index}`);
2120
- const newField = document.createElement('div');
2121
- newField.className = 'option-field';
2122
- newField.innerHTML = `
2123
- <input type="text" name="option_names" placeholder="Название опции">
2124
- <input type="text" name="option_prices" placeholder="Цена опции">
2125
- `;
2126
- container.appendChild(newField);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2127
  }
2128
 
2129
- function editProduct(index) {
2130
- const form = document.getElementById(`edit-form-${index}`);
2131
- form.style.display = form.style.display === 'none' ? 'block' : 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2132
  }
 
 
 
 
2133
  </script>
2134
  </body>
2135
  </html>
2136
  '''
2137
- return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID, qr_code=data['qr_code'])
 
2138
 
2139
  @app.route('/admin_login', methods=['GET', 'POST'])
2140
  def admin_login():
 
 
 
 
2141
  if request.method == 'POST':
2142
  login = request.form.get('login')
2143
  password = request.form.get('password')
2144
  if login == ADMIN_LOGIN and password == ADMIN_PASSWORD:
2145
- session['admin_login'] = login
2146
  return redirect(url_for('admin'))
2147
- return "Неверный логин или пароль", 401
2148
-
2149
- login_html = '''
2150
- <!DOCTYPE html>
2151
- <html lang="ru">
2152
- <head>
2153
- <meta charset="UTF-8">
2154
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
2155
- <title>Вход в админ-панель</title>
2156
- <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
2157
- <style>
2158
- body {
2159
- font-family: 'Poppins', sans-serif;
2160
- background: linear-gradient(135deg, #f0f2f5, #e9ecef);
2161
- color: #2d3748;
2162
- padding: 20px;
2163
- display: flex;
2164
- justify-content: center;
2165
- align-items: center;
2166
- height: 100vh;
2167
- margin: 0;
2168
- }
2169
- .login-container {
2170
- background: #fff;
2171
- padding: 30px;
2172
- border-radius: 15px;
2173
- box-shadow: 0 4px 15px rgba(0,0,0,0.1);
2174
- width: 100%;
2175
- max-width: 400px;
2176
- }
2177
- h2 {
2178
- font-weight: 600;
2179
- margin-bottom: 20px;
2180
- text-align: center;
2181
- }
2182
- label {
2183
- display: block;
2184
- margin: 10px 0 5px;
2185
- }
2186
- input {
2187
- width: 100%;
2188
- padding: 10px;
2189
- margin-bottom: 15px;
2190
- border: 1px solid #e2e8f0;
2191
- border-radius: 8px;
2192
- font-size: 1rem;
2193
- }
2194
- button {
2195
- width: 100%;
2196
- padding: 10px;
2197
- border: none;
2198
- border-radius: 8px;
2199
- background-color: #3b82f6;
2200
- color: white;
2201
- font-size: 1rem;
2202
- cursor: pointer;
2203
- }
2204
- button:hover {
2205
- background-color: #2563eb;
2206
- }
2207
- </style>
2208
- </head>
2209
- <body>
2210
- <div class="login-container">
2211
- <h2>Вход в админ-панель</h2>
2212
- <form method="POST">
2213
- <label for="login">Логин:</label>
2214
- <input type="text" name="login" id="login" required>
2215
- <label for="password">Пароль:</label>
2216
- <input type="password" name="password" id="password" required>
2217
- <button type="submit">Войти</button>
2218
- </form>
2219
- </div>
2220
- </body>
2221
- </html>
2222
- '''
2223
- return render_template_string(login_html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2224
 
2225
- @app.route('/admin_logout')
2226
  def admin_logout():
2227
- session.pop('admin_login', None)
2228
  return redirect(url_for('admin_login'))
2229
 
2230
- # --- User Authentication Routes ---
 
 
 
 
 
 
 
 
 
 
 
 
2231
  @app.route('/register', methods=['POST'])
2232
  def register():
2233
  login = request.form.get('registerLogin')
@@ -2240,13 +2342,13 @@ def register():
2240
  return jsonify({'status': 'error', 'message': message}), 400
2241
 
2242
  @app.route('/login', methods=['POST'])
2243
- def login():
2244
  login = request.form.get('loginUsername')
2245
  password = request.form.get('loginPassword')
2246
  user = authenticate_user(login, password)
2247
  if user:
2248
  session['user_login'] = login
2249
- return jsonify({'status': 'success', 'message': 'Вход успешен'})
2250
  return jsonify({'status': 'error', 'message': 'Неверный логин или пароль'}), 401
2251
 
2252
  @app.route('/logout')
@@ -2255,9 +2357,9 @@ def logout():
2255
  return redirect(url_for('menu'))
2256
 
2257
  @app.route('/update_profile', methods=['POST'])
2258
- def update_profile():
2259
  if 'user_login' not in session:
2260
- return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401
2261
  login = session['user_login']
2262
  phone = request.form.get('editPhone')
2263
  address = request.form.get('editAddress')
@@ -2269,17 +2371,21 @@ def update_profile():
2269
  @app.route('/redeem_points', methods=['POST'])
2270
  def redeem_points():
2271
  if 'user_login' not in session:
2272
- return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401
2273
  login = session['user_login']
2274
  points_to_redeem = int(request.form.get('points', 0))
2275
  success, message = redeem_points_from_user(login, points_to_redeem)
2276
  if success:
2277
- return jsonify({'status': 'success', 'message': 'Баллы списаны'})
2278
- return jsonify({'status': 'error', 'message': message}), 400
2279
 
2280
- # Запуск периодического резервного копирования в отдельном потоке
2281
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
2282
- backup_thread.start()
2283
 
2284
  if __name__ == '__main__':
 
 
 
 
 
 
 
2285
  app.run(debug=True, host='0.0.0.0', port=7860)
 
11
  from werkzeug.security import generate_password_hash, check_password_hash
12
 
13
  app = Flask(__name__)
14
+ app.secret_key = os.getenv("FLASK_SECRET_KEY", "your_default_secret_key") # для session
15
 
16
  DATA_FILE = 'data_exmenu.json'
17
  USER_DATA_FILE = 'data_emirusers.json'
 
21
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
22
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
23
  LOGO_URL = "https://huggingface.co/spaces/kgmenu/Example/resolve/main/emir_chaihana_14040103_125008071.jpg"
24
+ ADMIN_LOGIN = os.getenv("ADMIN_LOGIN", "admin")
25
+ ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin_password")
26
+
27
  # Настройка логирования
28
  logging.basicConfig(level=logging.DEBUG)
29
 
 
250
  return False, "Недостаточно баллов для списания."
251
  return False, "Пользователь не найден."
252
 
253
+
254
  @app.route('/')
255
  def menu():
256
  data = load_data()
 
259
  stoplist = data['stoplist']
260
  category_counts = get_category_counts(products)
261
 
 
262
  current_time = datetime.now()
263
  for product_id, stop_info in list(stoplist.items()):
264
  if stop_info['until'] <= current_time:
 
707
  text-align: center;
708
  margin: 5px 0;
709
  }
 
 
 
 
 
 
 
710
  .footer-info {
711
  text-align: center;
712
  margin-top: 20px;
 
771
  .logout-button:hover {
772
  background-color: #dc2626;
773
  }
774
+
775
  </style>
776
  </head>
777
  <body>
 
816
  <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
817
  <div id="stop-status-{{ loop.index0 }}">
818
  {% if stoplist[loop.index0|string] %}
819
+ <p class="stop-notice" id="stop-timer-{{ loop.index0 }}">Извините, блюдо на стопе, будет готово в течении <span class="timer" data-until="{{ stoplist[loop.index0|string]['until'] }}"></span></p>
820
  {% else %}
821
  <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
822
  <button class="product-button add-to-cart" onclick="openOptionsModal({{ loop.index0 }})">В корзину</button>
 
972
  </div>
973
  </div>
974
 
975
+
976
  <button id="cart-button" onclick="openCartModal()">🛒</button>
977
 
978
  <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
 
1145
  }
1146
  }
1147
 
1148
+
1149
  function orderViaWhatsApp() {
1150
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1151
  if (cart.length === 0) {
 
1192
  window.open(`https://api.whatsapp.com/send?phone=+996500131380&text=${orderText}`, '_blank');
1193
  }
1194
 
1195
+
1196
  function orderViaWhatsAppWithQR() {
1197
  const cart = JSON.parse(localStorage.getItem('cart') || '[]');
1198
  if (cart.length === 0) {
 
1222
  }
1223
  total -= redeemedPoints;
1224
  orderText += `%0AСписано баллов: ${redeemedPoints} с`;
1225
+ $.post('/redeem_points', { points: redeemedPoints }, function(response) {
1226
  if (response.status === 'success') {
1227
  availablePoints -= redeemedPoints;
1228
  document.getElementById('availablePoints').textContent = availablePoints;
 
1292
 
1293
  function startTimer(productId, until) {
1294
  const timerEl = document.querySelector(`#stop-timer-${productId} .timer`);
1295
+ if (!timerEl) return;
 
 
 
1296
 
1297
  function updateTimer() {
1298
  const remaining = new Date(until) - new Date();
1299
  if (remaining > 0) {
1300
  const minutes = Math.floor(remaining / 60000);
1301
  const seconds = Math.floor((remaining % 60000) / 1000);
1302
+ timerEl.textContent = `в течении ${minutes} минут`; // Modified timer text
1303
  } else {
1304
  clearInterval(timerInterval);
1305
  const stopStatus = document.getElementById(`stop-status-${productId}`);
 
1308
  <button class="product-button add-to-cart" onclick="openOptionsModal(${productId})">В корзину</button>
1309
  `;
1310
  delete stoplist[productId];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1311
  }
1312
  }
1313
  updateTimer();
 
1316
 
1317
  // Запускаем таймеры только для активных стопов
1318
  Object.entries(stoplist).forEach(([id, stopInfo]) => {
 
1319
  startTimer(id, stopInfo.until);
1320
  });
1321
 
 
1389
  }
1390
  });
1391
  });
1392
+
1393
+
1394
  </script>
1395
  </body>
1396
  </html>
 
1452
  products = data['products']
1453
  stoplist = data['stoplist']
1454
 
 
 
 
 
 
 
 
1455
  if request.method == 'POST':
1456
  action = request.form.get('action')
1457
  if action == 'add':
1458
  product_id = request.form.get('product_id')
1459
  minutes = int(request.form.get('minutes', 0))
1460
  if minutes > 0:
 
 
1461
  stoplist[product_id] = {
1462
+ 'until': datetime.now() + timedelta(minutes=minutes)
1463
  }
1464
+ save_data(data)
1465
+ return jsonify({'status': 'success', 'until': stoplist[product_id]['until'].isoformat()})
 
1466
  return jsonify({'status': 'error', 'message': 'Invalid minutes'}), 400
1467
  elif action == 'remove':
1468
  product_id = request.form.get('product_id')
 
1472
  return jsonify({'status': 'success'})
1473
  return jsonify({'status': 'error', 'message': 'Product not in stoplist'}), 404
1474
 
1475
+
1476
  stoplist_for_template = {
1477
  k: {'until': v['until'].isoformat() if isinstance(v['until'], datetime) else v['until']}
1478
  for k, v in stoplist.items()
 
1538
  .timer {
1539
  color: #ef4444;
1540
  font-weight: 500;
 
 
 
1541
  }
1542
  .stop-notice {
1543
  color: #ef4444;
 
1561
  <h3>{{ product['name'] }}</h3>
1562
  <div class="stop-status" id="stop-status-{{ loop.index0 }}">
1563
  {% if stoplist[loop.index0|string] %}
1564
+ <p class="stop-notice" id="stop-timer-{{ loop.index0 }}">Извините, блюдо на стопе, будет готово в течении <span class="timer" data-until="{{ stoplist[loop.index0|string]['until'] }}"></span></p>
1565
  {% else %}
1566
  <form class="stop-form" data-id="{{ loop.index0 }}">
1567
  <button type="button" onclick="addToStoplist({{ loop.index0 }}, 30)">30 мин</button>
 
1595
  if (response.status === 'success') {
1596
  stoplist[productId] = { until: response.until };
1597
  const stopStatus = document.getElementById(`stop-status-${productId}`);
1598
+ stopStatus.innerHTML = `<p class="stop-notice" id="stop-timer-${productId}">Извините, блюдо на стопе, будет готово в течении <span class="timer" data-until="${response.until}"></span></p>`;
1599
  let productItem = document.querySelector(`.product-item[data-id='${productId}']`);
1600
  let removeButton = document.createElement('button');
1601
  removeButton.className = 'remove-stop-button';
1602
  removeButton.textContent = 'Снять стоп';
1603
  removeButton.onclick = function() { removeFromStoplist(productId); };
1604
  productItem.appendChild(removeButton);
1605
+
1606
+
1607
  startTimer(productId, response.until);
1608
  } else {
1609
+ alert('Ошибка при добавлении в стоп-лист');
1610
  }
1611
  },
1612
  error: function() {
1613
+ alert('Ошибка сервера');
1614
  }
1615
  });
1616
  }
 
1640
  productItem.removeChild(removeButton);
1641
  }
1642
  } else {
1643
+ alert('Ошибка при снятии стоп-листа');
1644
  }
1645
  },
1646
  error: function() {
1647
+ alert('Ошибка сервера');
1648
  }
1649
  });
1650
  }
1651
 
1652
+
1653
  function startTimer(productId, until) {
1654
  const timerEl = document.querySelector(`#stop-timer-${productId} .timer`);
1655
+ if (!timerEl) return;
 
 
 
1656
 
1657
  function updateTimer() {
1658
  const remaining = new Date(until) - new Date();
1659
  if (remaining > 0) {
1660
  const minutes = Math.floor(remaining / 60000);
1661
  const seconds = Math.floor((remaining % 60000) / 1000);
1662
+ timerEl.textContent = `в течении ${minutes} минут`; // Modified timer text
1663
  } else {
1664
  clearInterval(timerInterval);
1665
+ const stopStatus = document.getElementById(`stop-status-${productId}`);
1666
+ stopStatus.innerHTML = `
1667
+ <form class="stop-form" data-id="${productId}">
1668
+ <button type="button" onclick="addToStoplist(${productId}, 30)">30 мин</button>
1669
+ <button type="button" onclick="addToStoplist(${productId}, 60)">60 мин</button>
1670
+ <button type="button" onclick="addToStoplist(${productId}, 90)">90 мин</button>
1671
+ </form>
1672
+ `;
1673
+ let productItem = document.querySelector(`.product-item[data-id='${productId}']`);
1674
+ let removeButton = productItem.querySelector('.remove-stop-button');
1675
+ if (removeButton) {
1676
+ productItem.removeChild(removeButton);
1677
+ }
1678
+ delete stoplist[productId];
1679
  }
1680
  }
1681
  updateTimer();
1682
  const timerInterval = setInterval(updateTimer, 1000);
1683
  }
1684
 
1685
+ // Запускаем таймеры только для активных стопов
1686
  Object.entries(stoplist).forEach(([id, stopInfo]) => {
 
1687
  startTimer(id, stopInfo.until);
1688
  });
1689
  </script>
1690
  </body>
1691
  </html>
1692
  '''
1693
+ return render_template_string(stoplist_html, products=products, stoplist=stoplist_for_template, stoplist_for_js=stoplist_for_js)
1694
 
 
1695
  @app.route('/admin', methods=['GET', 'POST'])
1696
  def admin():
1697
+ # No login required for /admin route as per user request
 
 
1698
  data = load_data()
1699
  products = data['products']
1700
  categories = data['categories']
 
1708
  if category_name and category_name not in categories:
1709
  categories.append(category_name)
1710
  save_data(data)
1711
+ return redirect(url_for('admin'))
1712
+ return "Ошибка: Категория уже существует или не указано название", 400
1713
 
1714
  elif action == 'delete_category':
1715
  category_index = int(request.form.get('category_index'))
1716
+ deleted_category = categories.pop(category_index)
 
 
1717
  for product in products:
1718
+ if product.get('category') == deleted_category:
1719
  product['category'] = 'Без категории'
1720
  save_data(data)
1721
  return redirect(url_for('admin'))
1722
 
1723
  elif action == 'add':
1724
+ # ... (rest of the 'add' action logic is the same) ...
1725
  name = request.form.get('name')
1726
+ price = request.form.get('price')
1727
  description = request.form.get('description')
1728
  category = request.form.get('category')
1729
  photos_files = request.files.getlist('photos')
 
1732
  photos_list = []
1733
  options_list = []
1734
 
 
1735
  if photos_files:
1736
  for photo in photos_files[:10]:
1737
  if photo and photo.filename:
 
1753
  if os.path.exists(temp_path):
1754
  os.remove(temp_path)
1755
 
 
1756
  for opt_name, opt_price in zip(option_names, option_prices):
1757
  if opt_name and opt_price:
1758
  options_list.append({
 
1760
  'price': float(opt_price.replace(',', '.'))
1761
  })
1762
 
1763
+ if not name or not price or not description:
1764
+ return "Ошибка: Заполните все обязательные поля", 400
1765
+
1766
+ price = float(price.replace(',', '.'))
1767
  new_product = {
1768
  'name': name,
1769
  'price': price,
 
1777
  return redirect(url_for('admin'))
1778
 
1779
  elif action == 'edit':
1780
+ # ... (rest of the 'edit' action logic is the same) ...
1781
+ index = int(request.form.get('index'))
1782
  name = request.form.get('name')
1783
+ price = request.form.get('price')
1784
  description = request.form.get('description')
1785
  category = request.form.get('category')
1786
  photos_files = request.files.getlist('photos')
1787
  option_names = request.form.getlist('option_names')
1788
  option_prices = request.form.getlist('option_prices')
 
 
1789
 
1790
+ if photos_files and any(photo.filename for photo in photos_files):
1791
+ new_photos_list = []
1792
  for photo in photos_files[:10]:
1793
  if photo and photo.filename:
1794
  photo_filename = secure_filename(photo.filename)
 
1803
  repo_id=REPO_ID,
1804
  repo_type="dataset",
1805
  token=HF_TOKEN_WRITE,
1806
+ commit_message=f"Обновлено фото для блюда {name}"
1807
  )
1808
+ new_photos_list.append(photo_filename)
1809
  if os.path.exists(temp_path):
1810
  os.remove(temp_path)
1811
+ products[index]['photos'] = new_photos_list
1812
 
1813
+ options_list = []
1814
  for opt_name, opt_price in zip(option_names, option_prices):
1815
  if opt_name and opt_price:
1816
  options_list.append({
 
1818
  'price': float(opt_price.replace(',', '.'))
1819
  })
1820
 
1821
+ products[index]['name'] = name
1822
+ products[index]['price'] = float(price.replace(',', '.'))
1823
+ products[index]['description'] = description
1824
+ products[index]['category'] = category if category in categories else 'Без категории'
1825
+ products[index]['options'] = options_list
 
 
 
1826
  save_data(data)
1827
  return redirect(url_for('admin'))
1828
 
1829
  elif action == 'delete':
1830
+ index = int(request.form.get('index'))
1831
+ del products[index]
1832
+ if str(index) in stoplist:
1833
+ del stoplist[str(index)]
 
 
 
 
 
 
 
 
 
 
 
1834
  save_data(data)
1835
  return redirect(url_for('admin'))
1836
 
1837
+ elif action == 'update_qr':
1838
+ qr_file = request.files.get('qr_code')
1839
  if qr_file and qr_file.filename:
1840
  qr_filename = secure_filename(qr_file.filename)
1841
  uploads_dir = 'uploads'
 
1849
  repo_id=REPO_ID,
1850
  repo_type="dataset",
1851
  token=HF_TOKEN_WRITE,
1852
+ commit_message="Обновлен QR-код"
1853
  )
1854
  data['qr_code'] = qr_filename
1855
  save_data(data)
1856
  if os.path.exists(temp_path):
1857
  os.remove(temp_path)
1858
+ return redirect(url_for('admin'))
1859
+
1860
+ stoplist_for_template = {
1861
+ k: {'until': v['until'].isoformat() if isinstance(v['until'], datetime) else v['until']}
1862
+ for k, v in stoplist.items()
1863
+ }
1864
 
1865
  admin_html = '''
1866
  <!DOCTYPE html>
 
1881
  max-width: 1200px;
1882
  margin: 0 auto;
1883
  }
1884
+ .header {
1885
+ display: flex;
1886
+ align-items: center;
1887
+ padding: 15px 0;
1888
+ border-bottom: 1px solid #e2e8f0;
1889
+ }
1890
+ .header-logo {
1891
+ width: 60px;
1892
+ height: 60px;
1893
+ border-radius: 50%;
1894
+ object-fit: cover;
1895
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
1896
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
1897
+ margin-right: 15px;
1898
+ }
1899
+ .header-logo:hover {
1900
+ transform: scale(1.1);
1901
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
1902
+ }
1903
  h1, h2 {
1904
  font-weight: 600;
1905
  margin-bottom: 20px;
1906
  }
1907
+ form {
1908
  background: #fff;
1909
  padding: 20px;
1910
  border-radius: 15px;
1911
  box-shadow: 0 4px 15px rgba(0,0,0,0.1);
1912
+ margin-bottom: 30px;
1913
  }
1914
  label {
1915
+ font-weight: 500;
1916
+ margin-top: 15px;
1917
  display: block;
 
1918
  }
1919
+ input, textarea, select {
1920
  width: 100%;
1921
+ padding: 12px;
1922
+ margin-top: 5px;
1923
  border: 1px solid #e2e8f0;
1924
  border-radius: 8px;
1925
  font-size: 1rem;
1926
+ transition: all 0.3s ease;
1927
+ }
1928
+ input:focus, textarea:focus, select:focus {
1929
+ border-color: #3b82f6;
1930
+ box-shadow: 0 0 5px rgba(59, 130, 246, 0.3);
1931
+ outline: none;
1932
  }
1933
  button {
1934
+ padding: 12px 20px;
1935
  border: none;
1936
  border-radius: 8px;
1937
  background-color: #3b82f6;
1938
  color: white;
1939
+ font-weight: 500;
1940
  cursor: pointer;
1941
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1942
+ margin-top: 15px;
1943
  }
1944
  button:hover {
1945
  background-color: #2563eb;
1946
+ box-shadow: 0 4px 15px rgba(37, 99, 235, 0.4);
1947
+ transform: translateY(-2px);
1948
  }
1949
  .delete-button {
1950
  background-color: #ef4444;
1951
  }
1952
  .delete-button:hover {
1953
  background-color: #dc2626;
1954
+ box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
1955
  }
1956
+ .product-list, .category-list {
1957
+ display: grid;
1958
+ gap: 20px;
1959
+ }
1960
+ .product-item, .category-item {
1961
+ background: #fff;
1962
+ padding: 20px;
1963
+ border-radius: 15px;
1964
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
1965
+ }
1966
+ .edit-form {
1967
+ margin-top: 15px;
1968
+ padding: 15px;
1969
+ background: #f7fafc;
1970
+ border-radius: 10px;
1971
  }
1972
+ .option-input-group {
1973
  display: flex;
1974
  gap: 10px;
1975
+ margin-top: 5px;
1976
  }
1977
+ .add-option-btn {
1978
+ background-color: #10b981;
1979
+ }
1980
+ .add-option-btn:hover {
1981
+ background-color: #059669;
1982
+ }
1983
+ .stop-notice {
1984
+ color: #ef4444;
1985
  }
1986
  </style>
1987
  </head>
1988
  <body>
1989
  <div class="container">
1990
+ <div class="header">
1991
+ <img src="''' + LOGO_URL + '''" alt="Логотип" class="header-logo">
1992
+ <h1>Админ-панель</h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
1993
  </div>
1994
+ <h1>Добавление блюда</h1>
1995
+ <form method="POST" enctype="multipart/form-data">
1996
+ <input type="hidden" name="action" value="add">
1997
+ <label>Название блюда:</label>
1998
+ <input type="text" name="name" required>
1999
+ <label>Цена:</label>
2000
+ <input type="number" name="price" step="0.01" required>
2001
+ <label>Описание:</label>
2002
+ <textarea name="description" rows="4" required></textarea>
2003
+ <label>Категория:</label>
2004
+ <select name="category">
2005
+ <option value="Без категории">Без категории</option>
2006
+ {% for category in categories %}
2007
+ <option value="{{ category }}">{{ category }}</option>
2008
+ {% endfor %}
2009
+ </select>
2010
+ <label>Фотографии (до 10):</label>
2011
+ <input type="file" name="photos" accept="image/*" multiple>
2012
+ <label>Дополнительные опции:</label>
2013
+ <div id="option-inputs">
2014
+ <div class="option-input-group">
2015
+ <input type="text" name="option_names" placeholder="Название опции">
2016
+ <input type="number" name="option_prices" step="0.01" value="0" placeholder="Цена">
2017
+ </div>
2018
+ </div>
2019
+ <button type="button" class="add-option-btn" onclick="addOptionInput()">Добавить опцию</button>
2020
+ <button type="submit">Добавить блюдо</button>
2021
+ </form>
2022
 
2023
+ <h1>Управление категориями</h1>
2024
+ <form method="POST">
2025
+ <input type="hidden" name="action" value="add_category">
2026
+ <label>Название категории:</label>
2027
+ <input type="text" name="category_name" required>
2028
+ <button type="submit">Добавить</button>
2029
+ </form>
2030
+
2031
+ <h2>Список категорий</h2>
2032
+ <div class="category-list">
2033
  {% for category in categories %}
2034
  <div class="category-item">
2035
+ <h3>{{ category }}</h3>
2036
+ <form method="POST" style="display: inline;">
2037
  <input type="hidden" name="action" value="delete_category">
2038
  <input type="hidden" name="category_index" value="{{ loop.index0 }}">
2039
  <button type="submit" class="delete-button">Удалить</button>
 
2042
  {% endfor %}
2043
  </div>
2044
 
2045
+ <h2>Управление QR-кодом</h2>
2046
+ <form method="POST" enctype="multipart/form-data">
2047
+ <input type="hidden" name="action" value="update_qr">
2048
+ <label>Загрузить QR-код:</label>
2049
+ <input type="file" name="qr_code" accept="image/*">
2050
+ <button type="submit">Обновить</button>
2051
+ </form>
2052
+ {% if qr_code %}
2053
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ qr_code }}" alt="QR-код" style="max-width: 200px;">
2054
+ {% endif %}
2055
+
2056
+ <h2>Управление базой данных</h2>
2057
+ <form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
2058
+ <button type="submit">Создать копию</button>
2059
+ </form>
2060
+ <form method="GET" action="{{ url_for('download') }}" style="display: inline;">
2061
+ <button type="submit">Скачать базу</button>
2062
+ </form>
2063
+ <form method="POST" action="{{ url_for('admin_logout') }}" style="display: inline;">
2064
+ <button type="submit">Выйти из админ-панели</button>
2065
+ </form>
2066
+
 
 
 
 
 
 
 
 
 
2067
 
2068
+ <h2>Список блюд</h2>
2069
+ <div class="product-list">
 
2070
  {% for product in products %}
2071
  <div class="product-item">
2072
+ <h3>{{ product['name'] }}</h3>
2073
+ <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
2074
+ <p><strong>Цена:</strong> {{ product['price'] }} с</p>
2075
+ <p><strong>Описание:</strong> {{ product['description'] }}</p>
2076
+ <p><strong>Опции:</strong>
2077
+ {% if product.get('options') and product['options']|length > 0 %}
2078
+ {{ product['options']|map(attribute='name')|join(', ') }}
2079
+ {% else %}
2080
+ Нет опций
2081
+ {% endif %}
2082
+ </p>
2083
+ {% if stoplist[loop.index0|string] %}
2084
+ <p class="stop-notice">На стопе до: <span class="timer" data-until="{{ stoplist[loop.index0|string]['until'] }}"></span> <button class="remove-stop-button" onclick="removeFromStoplist({{ loop.index0 }})">Снять стоп</button></p>
2085
+ {% endif %}
2086
+ {% if product.get('photos') and product['photos']|length > 0 %}
2087
+ <div style="display: flex; flex-wrap: wrap; gap: 10px;">
2088
+ {% for photo in product['photos'] %}
2089
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
2090
+ alt="{{ product['name'] }}"
2091
+ style="max-width: 100px; border-radius: 10px;">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2092
  {% endfor %}
2093
+ </div>
2094
+ {% endif %}
2095
+ <details>
2096
+ <summary>Редактировать</summary>
2097
+ <form method="POST" enctype="multipart/form-data" class="edit-form">
2098
+ <input type="hidden" name="action" value="edit">
2099
+ <input type="hidden" name="index" value="{{ loop.index0 }}">
2100
+ <label>Название:</label>
2101
+ <input type="text" name="name" value="{{ product['name'] }}" required>
2102
+ <label>Цена:</label>
2103
+ <input type="number" name="price" step="0.01" value="{{ product['price'] }}" required>
2104
+ <label>Описание:</label>
2105
+ <textarea name="description" rows="4" required>{{ product['description'] }}</textarea>
2106
+ <label>Категория:</label>
2107
+ <select name="category">
2108
+ <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
2109
+ {% for category in categories %}
2110
+ <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
2111
+ {% endfor %}
2112
+ </select>
2113
+ <label>Фотографии (до 10):</label>
2114
+ <input type="file" name="photos" accept="image/*" multiple>
2115
+ <label>Дополнительные опции:</label>
2116
+ <div id="edit-option-inputs-{{ loop.index0 }}">
2117
+ {% for option in product.get('options', []) %}
2118
+ <div class="option-input-group">
2119
+ <input type="text" name="option_names" value="{{ option['name'] }}">
2120
+ <input type="number" name="option_prices" step="0.01" value="{{ option['price'] }}">
2121
+ </div>
2122
+ {% endfor %}
2123
  </div>
2124
+ <button type="button" class="add-option-btn" onclick="addOptionInput('edit-option-inputs-{{ loop.index0 }}')">Добавить опцию</button>
2125
+ <button type="submit">Сохранить</button>
2126
+ </form>
2127
+ </details>
2128
+ <form method="POST">
2129
+ <input type="hidden" name="action" value="delete">
2130
+ <input type="hidden" name="index" value="{{ loop.index0 }}">
2131
+ <button type="submit" class="delete-button">Удалить</button>
2132
  </form>
2133
  </div>
2134
  {% endfor %}
2135
  </div>
2136
  </div>
2137
  <script>
2138
+ function addOptionInput(containerId = 'option-inputs') {
2139
+ const container = document.getElementById(containerId);
2140
+ const newInput = document.createElement('div');
2141
+ newInput.className = 'option-input-group';
2142
+ newInput.innerHTML = `
2143
  <input type="text" name="option_names" placeholder="Название опции">
2144
+ <input type="number" name="option_prices" step="0.01" value="0" placeholder="Цена">
2145
  `;
2146
+ container.appendChild(newInput);
2147
  }
2148
 
2149
+ let stoplist = {{ stoplist_for_template|tojson }};
2150
+
2151
+ function removeFromStoplist(productId) {
2152
+ $.post('/stoplist', {action: 'remove', product_id: productId}, function(response) {
2153
+ if (response.status === 'success') {
2154
+ let stopStatusContainer = document.getElementById(`stop-status-${productId}`);
2155
+ stopStatusContainer.innerHTML = `
2156
+ <form class="stop-form" data-id="${productId}">
2157
+ <button type="button" onclick="addToStoplist(${productId}, 30)">30 мин</button>
2158
+ <button type="button" onclick="addToStoplist(${productId}, 60)">60 мин</button>
2159
+ <button type="button" onclick="addToStoplist(${productId}, 90)">90 мин</button>
2160
+ </form>
2161
+ `;
2162
+ let productItem = document.querySelector(`.product-item[data-id='${productId}']`);
2163
+ let removeButton = productItem.querySelector('.remove-stop-button');
2164
+ if (removeButton) {
2165
+ productItem.removeChild(removeButton);
2166
+ }
2167
+ delete stoplist[productId];
2168
+ } else {
2169
+ alert('Ошибка при снятии стоп-листа');
2170
+ }
2171
+ });
2172
  }
2173
 
2174
+
2175
+ function startTimer(productId, until) {
2176
+ const timerEl = document.querySelector(`.product-item [data-until="${until}"] .timer`);
2177
+ if (!timerEl) return;
2178
+
2179
+ function updateTimer() {
2180
+ const remaining = new Date(until) - new Date();
2181
+ if (remaining > 0) {
2182
+ const minutes = Math.floor(remaining / 60000);
2183
+ const seconds = Math.floor((remaining % 60000) / 1000);
2184
+ timerEl.textContent = `в течении ${minutes} минут`; // Modified timer text
2185
+ } else {
2186
+ clearInterval(timerInterval);
2187
+ timerEl.parentElement.textContent = 'Блюдо снова доступно';
2188
+ let productItem = document.querySelector(`.product-item[data-id='${productId}']`);
2189
+ let removeButton = productItem.querySelector('.remove-stop-button');
2190
+ if (removeButton) {
2191
+ productItem.removeChild(removeButton);
2192
+ }
2193
+ delete stoplist[productId];
2194
+ }
2195
+ }
2196
+ updateTimer();
2197
+ const timerInterval = setInterval(updateTimer, 1000);
2198
  }
2199
+
2200
+ Object.entries(stoplist).forEach(([id, stopInfo]) => {
2201
+ startTimer(id, stopInfo.until);
2202
+ });
2203
  </script>
2204
  </body>
2205
  </html>
2206
  '''
2207
+ return render_template_string(admin_html, products=products, categories=categories, stoplist=stoplist,
2208
+ stoplist_for_template=stoplist_for_template, repo_id=REPO_ID, qr_code=data['qr_code'])
2209
 
2210
  @app.route('/admin_login', methods=['GET', 'POST'])
2211
  def admin_login():
2212
+ if session.get('admin_logged_in') == True:
2213
+ return redirect(url_for('admin'))
2214
+
2215
+ message = ''
2216
  if request.method == 'POST':
2217
  login = request.form.get('login')
2218
  password = request.form.get('password')
2219
  if login == ADMIN_LOGIN and password == ADMIN_PASSWORD:
2220
+ session['admin_logged_in'] = True
2221
  return redirect(url_for('admin'))
2222
+ else:
2223
+ message = 'Неверный логин или пароль'
2224
+ return render_template_string('''
2225
+ <!DOCTYPE html>
2226
+ <html>
2227
+ <head>
2228
+ <title>Вход в админ-панель</title>
2229
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
2230
+ <style>
2231
+ body {
2232
+ font-family: 'Poppins', sans-serif;
2233
+ background: linear-gradient(135deg, #f0f2f5, #e9ecef);
2234
+ color: #2d3748;
2235
+ padding: 20px;
2236
+ display: flex;
2237
+ justify-content: center;
2238
+ align-items: center;
2239
+ min-height: 100vh;
2240
+ }
2241
+ .login-container {
2242
+ background: #fff;
2243
+ padding: 30px;
2244
+ border-radius: 15px;
2245
+ box-shadow: 0 8px 20px rgba(0,0,0,0.1);
2246
+ width: 90%;
2247
+ max-width: 400px;
2248
+ text-align: center;
2249
+ }
2250
+ h2 {
2251
+ font-weight: 600;
2252
+ margin-bottom: 20px;
2253
+ color: #3b82f6;
2254
+ }
2255
+ label {
2256
+ display: block;
2257
+ margin-bottom: 8px;
2258
+ font-weight: 500;
2259
+ text-align: left;
2260
+ }
2261
+ input[type="text"], input[type="password"] {
2262
+ width: 100%;
2263
+ padding: 12px;
2264
+ margin-bottom: 20px;
2265
+ border: 1px solid #e2e8f0;
2266
+ border-radius: 8px;
2267
+ font-size: 1rem;
2268
+ transition: border-color 0.3s, box-shadow 0.3s;
2269
+ box-sizing: border-box;
2270
+ }
2271
+ input[type="text"]:focus, input[type="password"]:focus {
2272
+ border-color: #3b82f6;
2273
+ box-shadow: 0 0 5px rgba(59, 130, 246, 0.5);
2274
+ outline: none;
2275
+ }
2276
+ button {
2277
+ padding: 12px 25px;
2278
+ border: none;
2279
+ border-radius: 8px;
2280
+ background-color: #3b82f6;
2281
+ color: white;
2282
+ font-weight: 500;
2283
+ cursor: pointer;
2284
+ transition: background-color 0.3s, transform 0.3s, box-shadow 0.3s;
2285
+ }
2286
+ button:hover {
2287
+ background-color: #2563eb;
2288
+ transform: translateY(-2px);
2289
+ box-shadow: 0 4px 10px rgba(37, 99, 235, 0.4);
2290
+ }
2291
+ .error-message {
2292
+ color: #ef4444;
2293
+ margin-top: 10px;
2294
+ }
2295
+ </style>
2296
+ </head>
2297
+ <body>
2298
+ <div class="login-container">
2299
+ <h2>Вход в админ-панель</h2>
2300
+ {% if message %}
2301
+ <p class="error-message">{{ message }}</p>
2302
+ {% endif %}
2303
+ <form method="post">
2304
+ <label for="login">Логин:</label>
2305
+ <input type="text" id="login" name="login" required>
2306
+ <label for="password">Пароль:</label>
2307
+ <input type="password" id="password" name="password" required>
2308
+ <button type="submit">Войти</button>
2309
+ </form>
2310
+ </div>
2311
+ </body>
2312
+ </html>
2313
+ ''', message=message)
2314
 
2315
+ @app.route('/admin_logout', methods=['POST'])
2316
  def admin_logout():
2317
+ session.pop('admin_logged_in', None)
2318
  return redirect(url_for('admin_login'))
2319
 
2320
+
2321
+ @app.route('/backup', methods=['POST'])
2322
+ def backup():
2323
+ upload_db_to_hf()
2324
+ upload_user_db_to_hf()
2325
+ return "Резервная копия создана.", 200
2326
+
2327
+ @app.route('/download', methods=['GET'])
2328
+ def download():
2329
+ download_db_from_hf()
2330
+ download_user_db_from_hf()
2331
+ return "База данных скачана.", 200
2332
+
2333
  @app.route('/register', methods=['POST'])
2334
  def register():
2335
  login = request.form.get('registerLogin')
 
2342
  return jsonify({'status': 'error', 'message': message}), 400
2343
 
2344
  @app.route('/login', methods=['POST'])
2345
+ def login_route():
2346
  login = request.form.get('loginUsername')
2347
  password = request.form.get('loginPassword')
2348
  user = authenticate_user(login, password)
2349
  if user:
2350
  session['user_login'] = login
2351
+ return jsonify({'status': 'success'})
2352
  return jsonify({'status': 'error', 'message': 'Неверный логин или пароль'}), 401
2353
 
2354
  @app.route('/logout')
 
2357
  return redirect(url_for('menu'))
2358
 
2359
  @app.route('/update_profile', methods=['POST'])
2360
+ def update_profile_route():
2361
  if 'user_login' not in session:
2362
+ return jsonify({'status': 'error', 'message': 'Не авторизован'}), 403
2363
  login = session['user_login']
2364
  phone = request.form.get('editPhone')
2365
  address = request.form.get('editAddress')
 
2371
  @app.route('/redeem_points', methods=['POST'])
2372
  def redeem_points():
2373
  if 'user_login' not in session:
2374
+ return jsonify({'status': 'error', 'message': 'Не авторизован'}), 403
2375
  login = session['user_login']
2376
  points_to_redeem = int(request.form.get('points', 0))
2377
  success, message = redeem_points_from_user(login, points_to_redeem)
2378
  if success:
2379
+ return jsonify({'status': 'success'})
2380
+ return jsonify({'status': 'error', 'message': message or "Ошибка списания баллов"}), 400
2381
 
 
 
 
2382
 
2383
  if __name__ == '__main__':
2384
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
2385
+ backup_thread.start()
2386
+ try:
2387
+ load_data()
2388
+ load_user_data()
2389
+ except Exception as e:
2390
+ logging.error(f"Не удалось загрузить базу данных: {e}")
2391
  app.run(debug=True, host='0.0.0.0', port=7860)