Update app.py
Browse files
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")
|
| 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 = "
|
|
|
|
| 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 }}">Извините, блюдо на стопе, будет готово
|
| 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 |
-
|
| 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 =
|
| 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':
|
| 1493 |
}
|
| 1494 |
-
save_data(data)
|
| 1495 |
-
|
| 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 }}">Извините, блюдо на стопе, будет готово
|
| 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}">Извините, блюдо на стопе, будет готово
|
| 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('Ошибка при добавлении в
|
| 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('Ошибка при
|
| 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 =
|
| 1696 |
} else {
|
| 1697 |
clearInterval(timerInterval);
|
| 1698 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 1715 |
|
| 1716 |
-
# --- Admin Routes ---
|
| 1717 |
@app.route('/admin', methods=['GET', 'POST'])
|
| 1718 |
def admin():
|
| 1719 |
-
|
| 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 |
-
|
|
|
|
| 1736 |
|
| 1737 |
elif action == 'delete_category':
|
| 1738 |
category_index = int(request.form.get('category_index'))
|
| 1739 |
-
|
| 1740 |
-
del categories[category_index]
|
| 1741 |
-
# Обновляем категорию у продуктов
|
| 1742 |
for product in products:
|
| 1743 |
-
if product.get('category') ==
|
| 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 =
|
| 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 |
-
|
|
|
|
| 1803 |
name = request.form.get('name')
|
| 1804 |
-
price =
|
| 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 |
-
|
| 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"
|
| 1830 |
)
|
| 1831 |
-
|
| 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[
|
| 1844 |
-
|
| 1845 |
-
|
| 1846 |
-
|
| 1847 |
-
|
| 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 |
-
|
| 1856 |
-
products
|
| 1857 |
-
|
| 1858 |
-
|
| 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 == '
|
| 1874 |
-
qr_file = request.files.get('
|
| 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="
|
| 1889 |
)
|
| 1890 |
data['qr_code'] = qr_filename
|
| 1891 |
save_data(data)
|
| 1892 |
if os.path.exists(temp_path):
|
| 1893 |
os.remove(temp_path)
|
| 1894 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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:
|
| 1925 |
}
|
| 1926 |
label {
|
|
|
|
|
|
|
| 1927 |
display: block;
|
| 1928 |
-
margin: 10px 0 5px;
|
| 1929 |
}
|
| 1930 |
-
input,
|
| 1931 |
width: 100%;
|
| 1932 |
-
padding:
|
| 1933 |
-
margin-
|
| 1934 |
border: 1px solid #e2e8f0;
|
| 1935 |
border-radius: 8px;
|
| 1936 |
font-size: 1rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1937 |
}
|
| 1938 |
button {
|
| 1939 |
-
padding:
|
| 1940 |
border: none;
|
| 1941 |
border-radius: 8px;
|
| 1942 |
background-color: #3b82f6;
|
| 1943 |
color: white;
|
|
|
|
| 1944 |
cursor: pointer;
|
| 1945 |
-
|
|
|
|
| 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 |
-
.
|
| 1957 |
-
display:
|
| 1958 |
-
|
| 1959 |
-
|
| 1960 |
-
|
| 1961 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1962 |
}
|
| 1963 |
-
.option-
|
| 1964 |
display: flex;
|
| 1965 |
gap: 10px;
|
| 1966 |
-
margin-
|
| 1967 |
}
|
| 1968 |
-
.option-
|
| 1969 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1970 |
}
|
| 1971 |
</style>
|
| 1972 |
</head>
|
| 1973 |
<body>
|
| 1974 |
<div class="container">
|
| 1975 |
-
<
|
| 1976 |
-
|
| 1977 |
-
|
| 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 |
-
<
|
| 1995 |
-
<
|
| 1996 |
-
<
|
| 1997 |
-
|
| 1998 |
-
|
| 1999 |
-
|
| 2000 |
-
|
| 2001 |
-
|
| 2002 |
-
|
| 2003 |
{% for category in categories %}
|
| 2004 |
<div class="category-item">
|
| 2005 |
-
<
|
| 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 |
-
<
|
| 2017 |
-
<
|
| 2018 |
-
<
|
| 2019 |
-
|
| 2020 |
-
|
| 2021 |
-
|
| 2022 |
-
|
| 2023 |
-
|
| 2024 |
-
|
| 2025 |
-
|
| 2026 |
-
|
| 2027 |
-
|
| 2028 |
-
|
| 2029 |
-
|
| 2030 |
-
|
| 2031 |
-
|
| 2032 |
-
|
| 2033 |
-
|
| 2034 |
-
|
| 2035 |
-
|
| 2036 |
-
|
| 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-
|
| 2049 |
-
<h2>Список блюд</h2>
|
| 2050 |
{% for product in products %}
|
| 2051 |
<div class="product-item">
|
| 2052 |
-
<
|
| 2053 |
-
|
| 2054 |
-
|
| 2055 |
-
<
|
| 2056 |
-
|
| 2057 |
-
|
| 2058 |
-
|
| 2059 |
-
|
| 2060 |
-
|
| 2061 |
-
|
| 2062 |
-
</
|
| 2063 |
-
|
| 2064 |
-
|
| 2065 |
-
|
| 2066 |
-
|
| 2067 |
-
|
| 2068 |
-
|
| 2069 |
-
<
|
| 2070 |
-
|
| 2071 |
-
|
| 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 |
-
|
| 2089 |
-
|
| 2090 |
-
|
| 2091 |
-
|
| 2092 |
-
|
| 2093 |
-
<
|
| 2094 |
-
|
| 2095 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2096 |
</div>
|
| 2097 |
-
{
|
| 2098 |
-
|
| 2099 |
-
|
| 2100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2101 |
</form>
|
| 2102 |
</div>
|
| 2103 |
{% endfor %}
|
| 2104 |
</div>
|
| 2105 |
</div>
|
| 2106 |
<script>
|
| 2107 |
-
function
|
| 2108 |
-
const container = document.getElementById(
|
| 2109 |
-
const
|
| 2110 |
-
|
| 2111 |
-
|
| 2112 |
<input type="text" name="option_names" placeholder="Название опции">
|
| 2113 |
-
<input type="
|
| 2114 |
`;
|
| 2115 |
-
container.appendChild(
|
| 2116 |
}
|
| 2117 |
|
| 2118 |
-
|
| 2119 |
-
|
| 2120 |
-
|
| 2121 |
-
|
| 2122 |
-
|
| 2123 |
-
|
| 2124 |
-
|
| 2125 |
-
|
| 2126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2127 |
}
|
| 2128 |
|
| 2129 |
-
|
| 2130 |
-
|
| 2131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2132 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2133 |
</script>
|
| 2134 |
</body>
|
| 2135 |
</html>
|
| 2136 |
'''
|
| 2137 |
-
return render_template_string(admin_html, products=products, categories=categories,
|
|
|
|
| 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['
|
| 2146 |
return redirect(url_for('admin'))
|
| 2147 |
-
|
| 2148 |
-
|
| 2149 |
-
|
| 2150 |
-
|
| 2151 |
-
|
| 2152 |
-
|
| 2153 |
-
|
| 2154 |
-
|
| 2155 |
-
|
| 2156 |
-
|
| 2157 |
-
|
| 2158 |
-
|
| 2159 |
-
|
| 2160 |
-
|
| 2161 |
-
|
| 2162 |
-
|
| 2163 |
-
|
| 2164 |
-
|
| 2165 |
-
|
| 2166 |
-
|
| 2167 |
-
|
| 2168 |
-
|
| 2169 |
-
|
| 2170 |
-
|
| 2171 |
-
|
| 2172 |
-
|
| 2173 |
-
|
| 2174 |
-
|
| 2175 |
-
|
| 2176 |
-
|
| 2177 |
-
|
| 2178 |
-
|
| 2179 |
-
|
| 2180 |
-
|
| 2181 |
-
|
| 2182 |
-
|
| 2183 |
-
|
| 2184 |
-
|
| 2185 |
-
|
| 2186 |
-
|
| 2187 |
-
|
| 2188 |
-
|
| 2189 |
-
|
| 2190 |
-
|
| 2191 |
-
|
| 2192 |
-
|
| 2193 |
-
|
| 2194 |
-
|
| 2195 |
-
|
| 2196 |
-
|
| 2197 |
-
|
| 2198 |
-
|
| 2199 |
-
|
| 2200 |
-
|
| 2201 |
-
|
| 2202 |
-
|
| 2203 |
-
|
| 2204 |
-
|
| 2205 |
-
|
| 2206 |
-
|
| 2207 |
-
|
| 2208 |
-
|
| 2209 |
-
|
| 2210 |
-
|
| 2211 |
-
|
| 2212 |
-
|
| 2213 |
-
|
| 2214 |
-
|
| 2215 |
-
|
| 2216 |
-
|
| 2217 |
-
|
| 2218 |
-
|
| 2219 |
-
|
| 2220 |
-
|
| 2221 |
-
|
| 2222 |
-
|
| 2223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2224 |
|
| 2225 |
-
@app.route('/admin_logout')
|
| 2226 |
def admin_logout():
|
| 2227 |
-
session.pop('
|
| 2228 |
return redirect(url_for('admin_login'))
|
| 2229 |
|
| 2230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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'
|
| 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
|
| 2259 |
if 'user_login' not in session:
|
| 2260 |
-
return jsonify({'status': 'error', 'message': 'Не авторизован'}),
|
| 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': 'Не авторизован'}),
|
| 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'
|
| 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)
|