admin / app.py
Kgshop's picture
Update app.py
aba7a54 verified
import os
import base64
import json
import threading
import time
from datetime import datetime
from uuid import uuid4
from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
from werkzeug.utils import secure_filename
from dotenv import load_dotenv
import requests
load_dotenv()
app = Flask(__name__)
app.secret_key = 'super_secret_key_store_app_123'
DATA_FILE = 'data.json'
SYNC_FILES = [DATA_FILE]
REPO_ID = os.getenv("REPO_ID", "Kgshop/hkcafe")
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
CURRENCY_CODE = 'сом'
def download_db_from_hf(specific_file=None, retries=3, delay=5):
token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
files_to_download = [specific_file] if specific_file else SYNC_FILES
all_successful = True
for file_name in files_to_download:
success = False
for attempt in range(retries + 1):
try:
hf_hub_download(
repo_id=REPO_ID,
filename=file_name,
repo_type="dataset",
token=token_to_use,
local_dir=".",
local_dir_use_symlinks=False,
force_download=True,
resume_download=False
)
success = True
break
except RepositoryNotFoundError:
return False
except HfHubHTTPError as e:
if e.response.status_code == 404:
if attempt == 0 and not os.path.exists(file_name):
try:
if file_name == DATA_FILE:
with open(file_name, 'w', encoding='utf-8') as f:
json.dump({'products': [], 'categories': [], 'orders': {}, 'settings': {
'cafe_name': 'HongKong',
'logo_url': 'https://huggingface.co/spaces/Metapp/Tech/resolve/main/1776929812446-019db944-b5db-7524-8f44-73942d70a0f8.png',
'wa_shift1': '+77470623684',
'wa_shift2': '+77470623684',
'active_shift': 1
}}, f)
except Exception:
pass
success = False
break
except requests.exceptions.RequestException:
pass
except Exception:
pass
if attempt < retries:
time.sleep(delay)
if not success:
all_successful = False
return all_successful
def upload_db_to_hf(specific_file=None):
if not HF_TOKEN_WRITE:
return
try:
api = HfApi()
files_to_upload = [specific_file] if specific_file else SYNC_FILES
for file_name in files_to_upload:
if os.path.exists(file_name):
try:
api.upload_file(
path_or_fileobj=file_name,
path_in_repo=file_name,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
except Exception:
pass
except Exception:
pass
def periodic_backup():
while True:
time.sleep(1800)
upload_db_to_hf()
def load_data():
default_data = {
'products': [],
'categories': [],
'orders': {},
'settings': {
'cafe_name': 'HongKong',
'logo_url': 'https://huggingface.co/spaces/Metapp/Tech/resolve/main/1776929812446-019db944-b5db-7524-8f44-73942d70a0f8.png',
'wa_shift1': '+77470623684',
'wa_shift2': '+77470623684',
'active_shift': 1
}
}
data = default_data
try:
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
if not isinstance(data, dict):
raise FileNotFoundError
if 'products' not in data: data['products'] = []
if 'categories' not in data: data['categories'] = []
if 'orders' not in data: data['orders'] = {}
if 'settings' not in data: data['settings'] = default_data['settings']
except (FileNotFoundError, json.JSONDecodeError):
if download_db_from_hf(specific_file=DATA_FILE):
try:
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
if 'products' not in data: data['products'] = []
if 'categories' not in data: data['categories'] = []
if 'orders' not in data: data['orders'] = {}
if 'settings' not in data: data['settings'] = default_data['settings']
except Exception:
data = default_data
else:
data = default_data
except Exception:
data = default_data
migrated_cats = []
for c in data.get('categories', []):
if isinstance(c, str):
migrated_cats.append({'name': c, 'icon': 'fas fa-utensils'})
else:
if 'icon' not in c: c['icon'] = 'fas fa-utensils'
migrated_cats.append(c)
data['categories'] = migrated_cats
for product in data['products']:
if 'product_id' not in product:
product['product_id'] = uuid4().hex
if not os.path.exists(DATA_FILE):
try:
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(default_data, f)
except Exception:
pass
return data
def save_data(data):
try:
if not isinstance(data, dict):
return
if 'products' not in data: data['products'] = []
if 'categories' not in data: data['categories'] = []
if 'orders' not in data: data['orders'] = {}
if 'settings' not in data: data['settings'] = {
'cafe_name': 'HongKong',
'logo_url': '',
'wa_shift1': '',
'wa_shift2': '',
'active_shift': 1
}
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
upload_db_to_hf(specific_file=DATA_FILE)
except Exception:
pass
CATALOG_TEMPLATE = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>{{ settings.cafe_name }} | Меню</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
:root { --primary: #D4AF37; --bg: #000000; --surface: #111111; --text: #ffffff; --text-muted: #888888; --border: #333333; --accent: #D4AF37; }
* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; -webkit-tap-highlight-color: transparent; }
body { background-color: var(--bg); color: var(--text); padding-bottom: calc(90px + env(safe-area-inset-bottom)); }
.top-logo-container { background: var(--surface); padding: max(25px, env(safe-area-inset-top)) 20px 20px; text-align: center; border-bottom: 1px solid var(--border); display: flex; justify-content: center; align-items: center; }
.top-logo { width: 110px; height: 110px; border-radius: 50%; object-fit: cover; border: 3px solid var(--primary); box-shadow: 0 4px 15px rgba(212, 175, 55, 0.3); }
.header { display: flex; align-items: center; justify-content: space-between; padding: 15px 20px; background: var(--surface); box-shadow: 0 2px 10px rgba(0,0,0,0.5); position: sticky; top: 0; z-index: 100; }
.header h1 { font-size: 1.4rem; font-weight: 700; letter-spacing: -0.5px; color: var(--primary); }
.back-btn { display: none; font-size: 1.2rem; cursor: pointer; color: var(--text); margin-right: 15px; padding: 5px; }
.search-bar { padding: 15px 20px; background: var(--surface); border-bottom: 1px solid var(--border); }
.search-container { position: relative; display: flex; align-items: center; background: #1a1a1a; border-radius: 12px; padding: 0 15px; border: 1px solid var(--border); transition: all 0.2s; }
.search-container:focus-within { border-color: var(--primary); background: #222; }
.search-container i { color: var(--text-muted); font-size: 0.9rem; }
.search-bar input { width: 100%; padding: 12px 10px; border: none; background: transparent; outline: none; font-size: 0.95rem; color: var(--text); }
.categories-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; padding: 20px; }
.category-item { background: var(--surface); padding: 20px 15px; border-radius: 16px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; cursor: pointer; border: 1px solid var(--border); transition: transform 0.2s, border-color 0.2s; text-align: center; }
.category-item:active { transform: scale(0.96); border-color: var(--primary); }
.category-item span.name { font-size: 0.95rem; font-weight: 600; line-height: 1.3; color: var(--text); }
.category-item span.count { color: var(--text-muted); font-size: 0.8rem; background: #1a1a1a; padding: 4px 10px; border-radius: 20px; }
.products-container { display: none; padding: 20px; flex-direction: column; gap: 15px; }
.product-card { background: var(--surface); border-radius: 16px; padding: 12px; display: flex; border: 1px solid var(--border); align-items: stretch; gap: 15px; width: 100%; }
.product-img-wrapper { position: relative; width: 110px; height: 110px; flex-shrink: 0; }
.product-img { width: 100%; height: 100%; border-radius: 12px; object-fit: cover; cursor: pointer; background: #1a1a1a; border: 1px solid var(--border); }
.photo-count { position: absolute; bottom: 5px; right: 5px; background: rgba(0,0,0,0.8); color: var(--primary); font-size: 0.7rem; padding: 2px 6px; border-radius: 10px; pointer-events: none; }
.product-info { flex-grow: 1; display: flex; flex-direction: column; justify-content: space-between; min-width: 0; padding: 5px 0; }
.product-title { font-size: 0.95rem; font-weight: 600; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; color: var(--text); }
.product-desc { font-size: 0.8rem; color: var(--text-muted); margin-top: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.product-bottom { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; flex-wrap: wrap; gap: 10px; }
.product-price { font-weight: 700; font-size: 1rem; color: var(--primary); }
.controls-wrapper { display: flex; gap: 8px; align-items: center; }
.quantity-control { display: flex; align-items: center; background: #1a1a1a; border-radius: 8px; overflow: hidden; border: 1px solid var(--border); }
.quantity-control button { border: none; background: transparent; width: 32px; height: 32px; font-size: 1.1rem; cursor: pointer; color: var(--primary); display: flex; align-items: center; justify-content: center; transition: background 0.2s; }
.quantity-control button:active { background: #333; }
.quantity-control input { width: 36px; height: 32px; border: none; text-align: center; background: transparent; font-weight: 600; font-size: 0.95rem; color: var(--text); outline: none; }
.quantity-control input[type="number"]::-webkit-inner-spin-button,
.quantity-control input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.quantity-control input[type="number"] { -moz-appearance: textfield; }
.cart-bar { position: fixed; bottom: 0; left: 0; width: 100%; background: var(--surface); border-top: 1px solid var(--border); padding: 15px 20px calc(15px + env(safe-area-inset-bottom)); display: none; justify-content: space-between; align-items: center; z-index: 100; border-top-left-radius: 20px; border-top-right-radius: 20px; }
.cart-info { display: flex; flex-direction: column; }
.cart-total { font-size: 1.25rem; font-weight: 800; color: var(--primary); }
.checkout-btn { background: var(--primary); color: #000; padding: 12px 28px; border: none; border-radius: 12px; font-weight: 700; font-size: 1rem; cursor: pointer; transition: transform 0.2s; }
.checkout-btn:active { transform: scale(0.95); }
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); z-index: 200; justify-content: center; align-items: flex-end; opacity: 0; transition: opacity 0.3s; }
.modal-overlay.active { opacity: 1; }
.modal-content { background: var(--surface); width: 100%; max-height: 85vh; border-radius: 24px 24px 0 0; border: 1px solid var(--border); padding: 25px 20px calc(25px + env(safe-area-inset-bottom)); overflow-y: auto; display: flex; flex-direction: column; transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1); }
.modal-overlay.active .modal-content { transform: translateY(0); }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; }
.modal-header h2 { font-size: 1.3rem; font-weight: 700; color: var(--primary); }
.modal-close { font-size: 1.5rem; cursor: pointer; border: none; background: #1a1a1a; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--text); }
.customer-form { display: flex; flex-direction: column; gap: 12px; margin-bottom: 25px; }
.customer-form input { padding: 14px; border: 1px solid var(--border); border-radius: 12px; font-size: 0.95rem; background: #1a1a1a; color: var(--text); outline: none; transition: border-color 0.2s; }
.customer-form input:focus { border-color: var(--primary); background: #222; }
.cart-item-list { display: flex; flex-direction: column; gap: 15px; margin-bottom: 25px; }
.cart-item { display: flex; justify-content: space-between; align-items: center; background: #1a1a1a; border: 1px solid var(--border); padding: 15px; border-radius: 12px; flex-wrap: wrap; gap: 10px; }
.cart-item-name { flex: 1; min-width: 120px; font-size: 0.95rem; font-weight: 500; line-height: 1.3; color: var(--text); }
.cart-item-controls { display: flex; align-items: center; background: var(--surface); border-radius: 8px; border: 1px solid var(--border); overflow: hidden; }
.cart-item-controls button { border: none; background: transparent; width: 30px; height: 30px; font-size: 1rem; cursor: pointer; color: var(--primary); }
.cart-item-controls button:active { background: #333; }
.cart-item-controls input { width: 35px; text-align: center; font-weight: 600; font-size: 0.9rem; border: none; background: transparent; color: var(--text); outline: none; }
.cart-item-controls input[type="number"]::-webkit-inner-spin-button,
.cart-item-controls input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.cart-item-controls input[type="number"] { -moz-appearance: textfield; }
.cart-item-price { font-weight: 700; color: var(--primary); min-width: 70px; text-align: right; }
.cart-item-delete { color: #ff4757; background: none; border: none; font-size: 1.1rem; cursor: pointer; padding: 5px; }
.confirm-btn { background: var(--accent); color: #000; width: 100%; padding: 16px; border: none; border-radius: 14px; font-size: 1.1rem; font-weight: 700; cursor: pointer; }
.welcome-modal { display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 500; justify-content: center; align-items: center; flex-direction: column; padding: 20px; }
.welcome-box { background: var(--surface); border: 1px solid var(--border); border-radius: 24px; padding: 30px; text-align: center; width: 100%; max-width: 400px; }
.welcome-box h2 { color: var(--primary); margin-bottom: 20px; font-size: 1.5rem; }
.welcome-btn { display: block; width: 100%; padding: 16px; margin-bottom: 15px; border-radius: 14px; border: none; font-size: 1.1rem; font-weight: 700; cursor: pointer; transition: transform 0.2s; }
.welcome-btn:active { transform: scale(0.95); }
.btn-dinein { background: var(--primary); color: #000; }
.btn-delivery { background: #1a1a1a; color: var(--primary); border: 1px solid var(--primary); }
.gallery-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #000; z-index: 300; justify-content: center; align-items: center; flex-direction: column; }
.gallery-close { position: absolute; top: max(20px, env(safe-area-inset-top)); right: 20px; color: var(--primary); font-size: 2rem; cursor: pointer; background: rgba(0,0,0,0.8); width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; border: 1px solid var(--primary); z-index: 302; }
.gallery-img-container { position: relative; width: 100%; height: 70vh; display: flex; align-items: center; justify-content: center; }
.gallery-img { max-width: 100%; max-height: 100%; object-fit: contain; }
.gallery-nav { position: absolute; top: 50%; transform: translateY(-50%); color: var(--primary); font-size: 2rem; background: rgba(0,0,0,0.8); border: 1px solid var(--primary); width: 50px; height: 50px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 301; }
.gallery-nav.prev { left: 10px; }
.gallery-nav.next { right: 10px; }
.gallery-dots { display: flex; gap: 8px; margin-top: 20px; }
.gallery-dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(255,255,255,0.3); transition: background 0.3s; }
.gallery-dot.active { background: var(--primary); }
.floating-socials { position: fixed; bottom: max(100px, calc(100px + env(safe-area-inset-bottom))); right: 15px; display: flex; flex-direction: column; gap: 12px; z-index: 90; }
.social-btn { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #000; font-size: 1.6rem; text-decoration: none; box-shadow: 0 4px 12px rgba(0,0,0,0.5); transition: transform 0.2s; border: none; cursor: pointer; }
.social-btn:active { transform: scale(0.9); }
.btn-float-ig { background: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%); color: #fff; }
.btn-float-map { background: #ff4757; color: #fff; }
@media (min-width: 768px) {
.categories-container { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
.products-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); }
.modal-content { max-width: 500px; margin: 0 auto; border-radius: 24px; top: 50%; transform: translateY(-50%) scale(0.9); bottom: auto; position: relative; max-height: 90vh; }
.modal-overlay.active .modal-content { transform: translateY(-50%) scale(1); }
.cart-bar { max-width: 500px; left: 50%; transform: translateX(-50%); border-radius: 20px 20px 0 0; }
}
</style>
</head>
<body>
<div class="welcome-modal" id="welcomeModal" style="display:none;">
<div class="welcome-box">
<h2>Добро пожаловать</h2>
<button class="welcome-btn btn-dinein" onclick="selectOrderType('dine_in')"><i class="fas fa-store"></i> Внутри ресторана</button>
<button class="welcome-btn btn-delivery" onclick="selectOrderType('delivery')"><i class="fas fa-motorcycle"></i> Доставка</button>
</div>
</div>
<div class="top-logo-container">
<img src="{{ settings.logo_url }}" class="top-logo" alt="Логотип">
</div>
<div class="header">
<div style="display: flex; align-items: center;">
<i class="fas fa-arrow-left back-btn" id="backBtn" onclick="showCategories()"></i>
<h1 id="pageTitle">Меню</h1>
</div>
</div>
<div class="search-bar" id="searchBar">
<div class="search-container">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" placeholder="Поиск блюд..." oninput="filterCategories()">
</div>
</div>
<div class="categories-container" id="categoriesContainer"></div>
<div class="products-container" id="productsContainer"></div>
<div class="floating-socials">
<a href="https://instagram.com/hongkong_milyanfan" class="social-btn btn-float-ig" target="_blank"><i class="fab fa-instagram"></i></a>
<button class="social-btn btn-float-map" onclick="openAddressModal()"><i class="fas fa-map-marker-alt"></i></button>
</div>
<div class="cart-bar" id="cartBar">
<div class="cart-info">
<span style="font-size: 0.85rem; color: var(--text-muted); font-weight: 500;">Сумма заказа:</span>
<span class="cart-total"><span id="cartTotalSum">0</span> {{ currency_code }}</span>
</div>
<button class="checkout-btn" onclick="openCartModal()">Корзина <i class="fas fa-shopping-bag" style="margin-left:5px;"></i></button>
</div>
<div class="modal-overlay" id="cartModal" onclick="if(event.target === this) closeCartModal()">
<div class="modal-content">
<div class="modal-header">
<h2>Ваш заказ</h2>
<button class="modal-close" onclick="closeCartModal()"><i class="fas fa-times"></i></button>
</div>
<div class="cart-item-list" id="cartItemList"></div>
<div class="customer-form" id="dineInFields" style="display:none;">
<input type="text" id="tableNum" placeholder="Номер столика" required>
</div>
<div class="customer-form" id="deliveryFields" style="display:none;">
<input type="text" id="custName" placeholder="Ваше Имя" required>
<input type="text" id="custPhone" placeholder="Номер телефона" required>
<input type="text" id="custAddress" placeholder="Адрес доставки" required>
</div>
<button class="confirm-btn" onclick="submitOrder()">Оформить заказ</button>
</div>
</div>
<div class="modal-overlay" id="addressModal" onclick="if(event.target === this) closeAddressModal()">
<div class="modal-content" style="max-height: 30vh; align-items: center; justify-content: center; text-align: center;">
<div class="modal-header" style="width: 100%; margin-bottom: 10px;">
<h2 style="margin: 0 auto;">Наш адрес</h2>
<button class="modal-close" style="position: absolute; right: 20px;" onclick="closeAddressModal()"><i class="fas fa-times"></i></button>
</div>
<div style="font-size: 1.2rem; font-weight: 600; padding: 20px; color: var(--text);">
<i class="fas fa-map-pin" style="color: var(--primary); margin-right: 10px; font-size: 1.5rem;"></i>
село Милянфан<br>ул. Фрунзе 99
</div>
</div>
</div>
<div class="gallery-modal" id="galleryModal">
<button class="gallery-close" onclick="closeGallery()"><i class="fas fa-times"></i></button>
<div class="gallery-img-container" id="gallerySwipeArea">
<button class="gallery-nav prev" onclick="prevPhoto(event)"><i class="fas fa-chevron-left"></i></button>
<img src="" class="gallery-img" id="galleryImage">
<button class="gallery-nav next" onclick="nextPhoto(event)"><i class="fas fa-chevron-right"></i></button>
</div>
<div class="gallery-dots" id="galleryDots"></div>
</div>
<script>
const products = {{ products_json|safe }};
const categoriesList = {{ categories_json|safe }};
const repoId = '{{ repo_id }}';
const currency = '{{ currency_code }}';
const cafeName = '{{ settings.cafe_name }}';
let cart = {};
let currentGalleryPhotos = [];
let currentGalleryIndex = 0;
let orderType = localStorage.getItem('orderType');
function init() {
if (!orderType) {
document.getElementById('welcomeModal').style.display = 'flex';
} else {
setOrderTypeUI(orderType);
}
renderCategories();
updateCartUI();
}
function selectOrderType(type) {
orderType = type;
localStorage.setItem('orderType', type);
document.getElementById('welcomeModal').style.display = 'none';
setOrderTypeUI(type);
}
function setOrderTypeUI(type) {
if (type === 'dine_in') {
document.getElementById('deliveryFields').style.display = 'none';
document.getElementById('dineInFields').style.display = 'flex';
} else {
document.getElementById('deliveryFields').style.display = 'flex';
document.getElementById('dineInFields').style.display = 'none';
}
}
function renderCategories() {
const container = document.getElementById('categoriesContainer');
const prodContainer = document.getElementById('productsContainer');
prodContainer.style.display = 'none';
container.style.display = 'grid';
document.getElementById('backBtn').style.display = 'none';
document.getElementById('pageTitle').innerText = 'Меню';
container.innerHTML = '';
categoriesList.forEach(cat => {
const catProducts = products.filter(p => p.category === cat.name);
const count = catProducts.length;
const div = document.createElement('div');
div.className = 'category-item';
div.onclick = () => showProducts(cat.name);
div.innerHTML = `
<div style="background: #1a1a1a; width: 50px; height: 50px; border-radius: 12px; border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; margin-bottom: 5px;">
<i class="${cat.icon}" style="font-size: 1.5rem; color: var(--primary);"></i>
</div>
<span class="name">${cat.name}</span>
<span class="count">${count} шт</span>
`;
container.appendChild(div);
});
}
function showCategories() {
document.getElementById('searchInput').value = '';
renderCategories();
}
function filterCategories() {
const query = document.getElementById('searchInput').value.toLowerCase();
if (!query) {
renderCategories();
return;
}
document.getElementById('categoriesContainer').style.display = 'none';
const container = document.getElementById('productsContainer');
container.style.display = 'flex';
document.getElementById('backBtn').style.display = 'block';
document.getElementById('pageTitle').innerText = 'Поиск';
container.innerHTML = '';
const matchedProducts = products.filter(p =>
p.name.toLowerCase().includes(query) ||
p.category.toLowerCase().includes(query)
);
if(matchedProducts.length === 0) {
container.innerHTML = '<div style="text-align:center; padding: 40px; color: var(--text-muted);">Ничего не найдено</div>';
} else {
matchedProducts.forEach(p => renderProductCard(p, container));
}
}
function renderProductCard(p, container) {
const qty = cart[p.product_id] ? cart[p.product_id].quantity : 0;
const hasPhotos = p.photos && p.photos.length > 0;
const photoUrl = hasPhotos
? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${p.photos[0]}`
: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMWExYTFhIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMzMzMzMzMiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0Ij7QndC10YIg0YTQvtGC0L48L3RleHQ+PC9zdmc+';
const photoIndicator = hasPhotos && p.photos.length > 1 ? `<div class="photo-count"><i class="fas fa-images"></i> ${p.photos.length}</div>` : '';
const imgClick = hasPhotos ? `onclick="openGallery('${p.product_id}')"` : '';
const descHtml = p.description ? `<div class="product-desc">${p.description}</div>` : '';
const div = document.createElement('div');
div.className = 'product-card';
div.innerHTML = `
<div class="product-img-wrapper" ${imgClick}>
<img src="${photoUrl}" class="product-img">
${photoIndicator}
</div>
<div class="product-info">
<div>
<div class="product-title">${p.name}</div>
${descHtml}
</div>
<div class="product-bottom">
<div class="product-price">${p.price} ${currency}</div>
<div class="controls-wrapper">
<div class="quantity-control">
<button onclick="updateCart('${p.product_id}', -1)"><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
<input type="number" id="qty-${p.product_id}" value="${qty}" onchange="manualUpdateCart('${p.product_id}', this.value)">
<button onclick="updateCart('${p.product_id}', 1)"><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
</div>
</div>
</div>
</div>
`;
container.appendChild(div);
}
function showProducts(category) {
document.getElementById('categoriesContainer').style.display = 'none';
const container = document.getElementById('productsContainer');
container.style.display = 'flex';
document.getElementById('backBtn').style.display = 'block';
document.getElementById('pageTitle').innerText = category;
container.innerHTML = '';
const catProducts = products.filter(p => p.category === category);
if(catProducts.length === 0) {
container.innerHTML = '<div style="text-align:center; padding: 40px; color: var(--text-muted);">В этой категории пока нет блюд</div>';
} else {
catProducts.forEach(p => renderProductCard(p, container));
}
}
function updateCart(productId, change, exactValue = null) {
const product = products.find(p => p.product_id === productId);
if (!product) return;
if (!cart[productId]) {
cart[productId] = { ...product, quantity: 0 };
}
if (exactValue !== null) {
cart[productId].quantity = exactValue;
} else {
cart[productId].quantity += change;
}
if (cart[productId].quantity <= 0) {
delete cart[productId];
const qtyInput = document.getElementById(`qty-${productId}`);
if (qtyInput) qtyInput.value = 0;
} else {
const qtyInput = document.getElementById(`qty-${productId}`);
if (qtyInput) qtyInput.value = cart[productId].quantity;
}
updateCartUI();
}
function manualUpdateCart(productId, val) {
let num = parseInt(val);
if (isNaN(num) || num < 0) num = 0;
updateCart(productId, 0, num);
}
function updateCartUI() {
let total = 0;
for (let id in cart) {
total += cart[id].price * cart[id].quantity;
}
const cartBar = document.getElementById('cartBar');
if (total > 0) {
cartBar.style.display = 'flex';
document.getElementById('cartTotalSum').innerText = total;
} else {
cartBar.style.display = 'none';
closeCartModal();
}
if (document.getElementById('cartModal').classList.contains('active')) {
renderCartModalItems();
}
}
function renderCartModalItems() {
const list = document.getElementById('cartItemList');
list.innerHTML = '';
for (let id in cart) {
const item = cart[id];
list.innerHTML += `
<div class="cart-item">
<div class="cart-item-name">
${item.name}
<div style="font-size: 0.8rem; color: var(--primary); margin-top:2px;">${item.quantity} шт.</div>
</div>
<div style="display:flex; align-items:center; gap: 10px;">
<div class="cart-item-controls">
<button onclick="updateCart('${id}', -1)"><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
<input type="number" value="${item.quantity}" onchange="manualUpdateCart('${id}', this.value)">
<button onclick="updateCart('${id}', 1)"><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
</div>
<button class="cart-item-delete" onclick="updateCart('${id}', 0, 0)"><i class="fas fa-trash-alt"></i></button>
</div>
<div class="cart-item-price">${item.price * item.quantity} ${currency}</div>
</div>
`;
}
}
function openCartModal() {
renderCartModalItems();
const modal = document.getElementById('cartModal');
modal.style.display = 'flex';
setTimeout(() => modal.classList.add('active'), 10);
}
function closeCartModal() {
const modal = document.getElementById('cartModal');
modal.classList.remove('active');
setTimeout(() => modal.style.display = 'none', 300);
}
function openAddressModal() {
const modal = document.getElementById('addressModal');
modal.style.display = 'flex';
setTimeout(() => modal.classList.add('active'), 10);
}
function closeAddressModal() {
const modal = document.getElementById('addressModal');
modal.classList.remove('active');
setTimeout(() => modal.style.display = 'none', 300);
}
function submitOrder() {
const cartArray = Object.values(cart);
if(cartArray.length === 0) return;
let name = "Не указано";
let phone = "Не указано";
let address = "Не указано";
let table = "Не указано";
if (orderType === 'dine_in') {
table = document.getElementById('tableNum').value.trim();
if(!table) {
alert('Пожалуйста, укажите номер столика');
return;
}
} else {
name = document.getElementById('custName').value.trim();
phone = document.getElementById('custPhone').value.trim();
address = document.getElementById('custAddress').value.trim();
if(!name || !phone || !address) {
alert('Пожалуйста, заполните все поля для доставки');
return;
}
}
const btn = document.querySelector('.confirm-btn');
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Оформление...';
btn.disabled = true;
fetch('/create_order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cart: cartArray,
order_type: orderType,
customer_name: name,
customer_phone: phone,
customer_address: address,
table_number: table
})
})
.then(r => r.json())
.then(data => {
if(data.order_id) {
cart = {};
window.location.href = `/order/${data.order_id}`;
}
})
.catch(() => {
btn.innerHTML = 'Оформить заказ';
btn.disabled = false;
alert('Произошла ошибка. Попробуйте еще раз.');
});
}
function openGallery(productId) {
const product = products.find(p => p.product_id === productId);
if (!product || !product.photos || product.photos.length === 0) return;
currentGalleryPhotos = product.photos.map(p => `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${p}`);
currentGalleryIndex = 0;
document.getElementById('galleryModal').style.display = 'flex';
document.body.style.overflow = 'hidden';
updateGalleryView();
}
function closeGallery() {
document.getElementById('galleryModal').style.display = 'none';
document.body.style.overflow = '';
}
function updateGalleryView() {
document.getElementById('galleryImage').src = currentGalleryPhotos[currentGalleryIndex];
const dotsContainer = document.getElementById('galleryDots');
dotsContainer.innerHTML = '';
if(currentGalleryPhotos.length > 1) {
currentGalleryPhotos.forEach((_, index) => {
const dot = document.createElement('div');
dot.className = `gallery-dot ${index === currentGalleryIndex ? 'active' : ''}`;
dotsContainer.appendChild(dot);
});
document.querySelectorAll('.gallery-nav').forEach(el => el.style.display = 'flex');
} else {
document.querySelectorAll('.gallery-nav').forEach(el => el.style.display = 'none');
}
}
function nextPhoto(e) {
if(e) e.stopPropagation();
if(currentGalleryPhotos.length <= 1) return;
currentGalleryIndex = (currentGalleryIndex + 1) % currentGalleryPhotos.length;
updateGalleryView();
}
function prevPhoto(e) {
if(e) e.stopPropagation();
if(currentGalleryPhotos.length <= 1) return;
currentGalleryIndex = (currentGalleryIndex - 1 + currentGalleryPhotos.length) % currentGalleryPhotos.length;
updateGalleryView();
}
let touchstartX = 0;
let touchendX = 0;
const swipeArea = document.getElementById('gallerySwipeArea');
swipeArea.addEventListener('touchstart', e => { touchstartX = e.changedTouches[0].screenX; });
swipeArea.addEventListener('touchend', e => {
touchendX = e.changedTouches[0].screenX;
if (touchstartX - touchendX > 50) nextPhoto();
if (touchendX - touchstartX > 50) prevPhoto();
});
init();
</script>
</body>
</html>
'''
ORDER_TEMPLATE = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Счет №{{ order.id }}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
:root { --bg: #f4f4f4; --surface: #ffffff; --text: #000000; --border: #000000; --wa: #25D366; --print: #1a1a1a; --primary: #000000; --text-muted: #555555; }
* { box-sizing: border-box; font-family: "Courier New", Courier, monospace; }
body { margin: 0; padding: max(20px, env(safe-area-inset-top)) 20px calc(100px + env(safe-area-inset-bottom)); background: var(--bg); display: flex; flex-direction: column; align-items: center; color: var(--text); }
.receipt-ticket { background: var(--surface); width: 100%; max-width: 450px; padding: 30px 20px; border: 1px solid #ddd; box-shadow: 0 4px 10px rgba(0,0,0,0.1); border-radius: 4px; position: relative; }
.receipt-ticket::before, .receipt-ticket::after { content: ''; position: absolute; left: 0; right: 0; height: 10px; background-size: 20px 20px; }
.receipt-ticket::before { top: -10px; background-image: radial-gradient(circle at 10px 0, transparent 10px, var(--surface) 11px); }
.receipt-ticket::after { bottom: -10px; background-image: radial-gradient(circle at 10px 10px, transparent 10px, var(--surface) 11px); }
.logo-area { text-align: center; margin-bottom: 20px; }
.logo-area img { max-height: 80px; filter: grayscale(100%); }
.cafe-name { text-align: center; font-size: 1.5rem; font-weight: bold; margin-bottom: 10px; text-transform: uppercase; }
.divider { border-top: 2px dashed var(--border); margin: 15px 0; }
.header-info { margin-bottom: 15px; font-size: 0.95rem; line-height: 1.4; }
.header-info div { display: flex; justify-content: space-between; }
.item-list { width: 100%; margin-bottom: 15px; }
.item-row { display: flex; flex-direction: column; margin-bottom: 10px; font-size: 0.95rem; }
.item-name { font-weight: bold; margin-bottom: 2px; }
.item-calc { display: flex; justify-content: space-between; padding-left: 10px; color: var(--text-muted); }
.totals-area { font-size: 1.1rem; }
.totals-row { display: flex; justify-content: space-between; margin-bottom: 5px; }
.totals-row.grand-total { font-size: 1.3rem; font-weight: bold; margin-top: 10px; padding-top: 10px; border-top: 2px dashed var(--border); }
.thank-you { text-align: center; margin-top: 30px; font-weight: bold; font-size: 1.1rem; }
.cart-item-controls { display: inline-flex; align-items: center; border: 1px solid #ccc; border-radius: 4px; overflow: hidden; margin-bottom: 5px; background: #fff; }
.cart-item-controls button { border: none; background: #eee; width: 25px; height: 25px; cursor: pointer; color: #000; font-weight: bold; }
.cart-item-controls input { width: 30px; text-align: center; border: none; background: transparent; color: #000; font-family: "Courier New", Courier, monospace; outline: none; }
.cart-item-controls input[type="number"]::-webkit-inner-spin-button,
.cart-item-controls input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.cart-item-controls input[type="number"] { -moz-appearance: textfield; }
.screen-only { display: block; }
.print-only { display: none; }
.action-bar { position: fixed; bottom: 0; left: 0; width: 100%; background: #fff; border-top: 1px solid #ddd; padding: 15px 20px calc(15px + env(safe-area-inset-bottom)); display: flex; gap: 15px; z-index: 100; justify-content: center; box-shadow: 0 -2px 10px rgba(0,0,0,0.1); }
.action-bar-inner { display: flex; gap: 15px; width: 100%; max-width: 450px; }
.btn { flex: 1; padding: 15px 10px; border-radius: 8px; border: none; font-size: 1rem; font-weight: bold; cursor: pointer; color: #fff; display: flex; align-items: center; justify-content: center; gap: 8px; font-family: sans-serif; text-transform: uppercase; }
.btn-wa { background: var(--wa); }
.btn-print { background: var(--print); }
.btn-home { background: #555; flex: 0 0 auto; padding: 15px 20px; }
#loadingOverlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 999; justify-content: center; align-items: center; font-size: 2rem; color: #fff; }
@media print {
body { background: #fff; padding: 0; align-items: flex-start; justify-content: flex-start; }
.receipt-ticket { box-shadow: none; border: none; padding: 0; max-width: 100%; }
.receipt-ticket::before, .receipt-ticket::after { display: none; }
.action-bar, .screen-only { display: none !important; }
.print-only { display: block !important; }
}
</style>
</head>
<body>
<div id="loadingOverlay"><i class="fas fa-spinner fa-spin"></i></div>
<div class="receipt-ticket">
<div class="logo-area">
<img src="{{ settings.logo_url }}" alt="Logo">
</div>
<div class="cafe-name">{{ settings.cafe_name }}</div>
<div class="divider"></div>
<div class="header-info">
<div><span>Чек №:</span> <span>{{ order.id }}</span></div>
<div><span>Дата:</span> <span>{{ order.created_at }}</span></div>
<div><span>Тип:</span> <span>{{ 'В заведении' if order.order_type == 'dine_in' else 'Доставка' }}</span></div>
<br>
{% if order.order_type == 'dine_in' %}
<div><span>Столик:</span> <span>{{ order.table_number }}</span></div>
{% else %}
<div><span>Гость:</span> <span>{{ order.customer_name }}</span></div>
<div><span>Телефон:</span> <span>{{ order.customer_phone }}</span></div>
<div><span>Адрес:</span> <span>{{ order.customer_address }}</span></div>
{% endif %}
<div><span>Статус:</span> <span>{{ 'ПОДТВЕРЖДЕН' if order.status == 'confirmed' else 'ОЖИДАЕТ' }}</span></div>
</div>
<div class="divider"></div>
<div class="item-list">
{% set raw_total = 0 %}
{% for item in order.cart %}
{% set item_sum = item.price * item.quantity %}
{% set raw_total = raw_total + item_sum %}
<div class="item-row">
<div class="item-name">{{ item.name }}</div>
<div class="item-calc">
<div class="screen-only">
<div style="display:flex; align-items:center; gap:10px;">
<div class="cart-item-controls">
<button onclick="updateItem('{{ item.product_id }}', -1)">-</button>
<input type="number" value="{{ item.quantity }}" onchange="manualUpdateOrder('{{ item.product_id }}', this.value)">
<button onclick="updateItem('{{ item.product_id }}', 1)">+</button>
</div>
<button onclick="updateItem('{{ item.product_id }}', 0, true)" style="color: #d9534f; background: none; border: none; font-size: 1.1rem; cursor: pointer; padding: 5px;"><i class="fas fa-times"></i></button>
</div>
</div>
<div class="print-only">
{{ item.quantity }} x {{ item.price }}
</div>
<div style="margin-top:auto; font-weight:bold;">{{ item_sum }} {{ currency_code }}</div>
</div>
</div>
{% endfor %}
</div>
<div class="divider"></div>
<div class="totals-area">
<div class="totals-row">
<span>Итого:</span>
<span>{{ raw_total }} {{ currency_code }}</span>
</div>
{% set discount = order.discount|default(0)|float %}
{% if discount > 0 %}
<div class="totals-row" style="color: #d9534f;">
<span>Скидка:</span>
<span>-{{ discount }} {{ currency_code }}</span>
</div>
{% endif %}
<div class="totals-row grand-total">
<span>К ОПЛАТЕ:</span>
<span>{{ order.total_price }} {{ currency_code }}</span>
</div>
</div>
<div class="divider"></div>
<div class="thank-you">
СПАСИБО ЗА ВАШ ВИЗИТ!
</div>
</div>
<div class="action-bar">
<div class="action-bar-inner">
<a href="/" class="btn btn-home"><i class="fas fa-home"></i></a>
<button class="btn btn-print" onclick="window.print()"><i class="fas fa-print"></i></button>
<button class="btn btn-wa" onclick="sendToWA()"><i class="fab fa-whatsapp"></i> Отправить</button>
</div>
</div>
<script>
function sendToWA() {
let msg = `Здравствуйте! Мой заказ №{{ order.id }}\nСсылка: ${window.location.href}`;
window.open(`https://api.whatsapp.com/send?phone={{ settings.wa_shift1|replace('+', '') if settings.active_shift == 1 else settings.wa_shift2|replace('+', '') }}&text=${encodeURIComponent(msg)}`, '_blank');
}
function updateItem(productId, change, isRemove = false) {
document.getElementById('loadingOverlay').style.display = 'flex';
fetch(`/edit_order/{{ order.id }}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product_id: productId, change: change, remove: isRemove })
})
.then(r => r.json())
.then(data => {
if(data.success) {
window.location.reload();
} else {
alert('Ошибка обновления');
document.getElementById('loadingOverlay').style.display = 'none';
}
})
.catch(() => {
alert('Произошла ошибка');
document.getElementById('loadingOverlay').style.display = 'none';
});
}
function manualUpdateOrder(productId, val) {
let num = parseInt(val);
if (isNaN(num) || num < 0) return;
document.getElementById('loadingOverlay').style.display = 'flex';
fetch(`/edit_order/{{ order.id }}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product_id: productId, exact_qty: num })
})
.then(r => r.json())
.then(data => {
if(data.success) {
window.location.reload();
} else {
alert('Ошибка обновления');
document.getElementById('loadingOverlay').style.display = 'none';
}
})
.catch(() => {
alert('Произошла ошибка');
document.getElementById('loadingOverlay').style.display = 'none';
});
}
</script>
</body>
</html>
'''
ADMIN_TEMPLATE = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Админ-панель</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
:root { --primary: #D4AF37; --bg: #000000; --surface: #111111; --border: #333333; --danger: #ff4757; --success: #2ed573; --info: #D4AF37; --warning: #ffa502; --text: #ffffff; --text-muted: #888888; }
* { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
body { background: var(--bg); padding: max(20px, env(safe-area-inset-top)) 15px calc(20px + env(safe-area-inset-bottom)); margin: 0; color: var(--text); }
.container { max-width: 1000px; margin: 0 auto; }
.header-panel { background: var(--surface); padding: 20px; border-radius: 16px; border: 1px solid var(--border); margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; }
.header-panel h1 { margin: 0; font-size: 1.5rem; font-weight: 800; color: var(--primary); }
.btn { padding: 12px 20px; border: none; border-radius: 10px; font-weight: 700; cursor: pointer; color: #000; text-decoration: none; display: inline-flex; align-items: center; gap: 8px; font-size: 0.95rem; transition: opacity 0.2s; }
.btn:active { opacity: 0.8; }
.btn-primary { background: var(--info); }
.btn-success { background: var(--success); }
.btn-danger { background: var(--danger); padding: 8px 15px; font-size: 0.85rem; color: #fff; }
.btn-warning { background: var(--warning); padding: 8px 15px; font-size: 0.85rem; color: #000; }
.btn-dark { background: #1a1a1a; color: var(--primary); border: 1px solid var(--border); }
.sync-panel { display: flex; gap: 10px; margin-bottom: 25px; flex-wrap: wrap; }
.sync-panel form { flex: 1; min-width: 200px; }
.sync-panel button { width: 100%; }
.card { background: var(--surface); padding: 20px; border-radius: 16px; border: 1px solid var(--border); margin-bottom: 20px; }
.card h2 { margin-top: 0; margin-bottom: 15px; font-size: 1.2rem; color: var(--primary); }
input[type="text"], input[type="number"], select, textarea { width: 100%; padding: 12px 15px; border: 1px solid var(--border); border-radius: 10px; font-size: 0.95rem; outline: none; transition: border-color 0.2s; background: #1a1a1a; color: var(--text); }
input[type="text"]:focus, input[type="number"]:focus, textarea:focus { border-color: var(--primary); background: #222; }
textarea { resize: vertical; min-height: 80px; font-family: inherit; }
label { display: block; margin-bottom: 5px; color: var(--text-muted); font-size: 0.85rem; font-weight: 600; }
.add-cat-form { display: flex; gap: 10px; flex-wrap: wrap; }
.add-cat-form input { flex: 1; min-width: 200px; }
.add-cat-form button { white-space: nowrap; }
.search-bar-admin { position: relative; margin-bottom: 20px; }
.search-bar-admin i { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: var(--text-muted); }
.search-bar-admin input { padding-left: 40px; background: var(--surface); border: 1px solid var(--border); }
.category-block { border: 1px solid var(--border); margin-bottom: 15px; border-radius: 12px; overflow: hidden; background: var(--surface); }
.category-header { background: #1a1a1a; padding: 15px 20px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.2s; }
.category-header:hover { background: #222; }
.category-content { padding: 0; display: none; }
.category-content.active { display: block; }
.product-item { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid var(--border); flex-wrap: wrap; gap: 10px; }
.product-item:last-child { border-bottom: none; }
.product-info { display: flex; align-items: center; gap: 15px; min-width: 250px; flex: 1; }
.product-img { width: 50px; height: 50px; object-fit: cover; border-radius: 8px; border: 1px solid #333; background: #1a1a1a; }
.product-details { display: flex; flex-direction: column; }
.product-name { font-weight: 600; font-size: 0.95rem; color: var(--text); }
.product-desc { font-size: 0.85rem; color: var(--text-muted); margin-top: 2px; }
.product-meta { font-size: 0.8rem; color: var(--primary); margin-top: 4px; }
.product-actions { display: flex; gap: 5px; }
.add-product-wrapper { display: none; }
.add-product-wrapper.active { display: block; }
.toggle-add-product { width: 100%; text-align: center; background: #1a1a1a; padding: 15px; cursor: pointer; color: var(--primary); font-weight: 600; transition: background 0.2s; border-bottom: 1px solid var(--border); }
.toggle-add-product:hover { background: #222; }
.add-product-form { background: var(--surface); padding: 20px; display: flex; flex-direction: column; gap: 15px; }
.form-row { display: flex; gap: 10px; flex-wrap: wrap; }
.form-row > * { flex: 1; min-width: 150px; }
.file-input-wrapper { position: relative; width: 100%; }
input[type="file"] { width: 100%; padding: 10px; border: 1px dashed var(--border); border-radius: 10px; background: #1a1a1a; color: var(--text); font-size: 0.9rem; }
.orders-table { width: 100%; border-collapse: collapse; min-width: 800px; text-align: left; }
.orders-table th { padding: 12px; background: #1a1a1a; border-bottom: 2px solid var(--border); color: var(--primary); font-size: 0.85rem; text-transform: uppercase; }
.orders-table td { padding: 12px; border-bottom: 1px solid var(--border); vertical-align: middle; color: var(--text); }
.orders-table tr:hover { background: #1a1a1a; }
.badge { padding: 4px 8px; border-radius: 6px; font-size: 0.75rem; font-weight: bold; text-transform: uppercase; }
.badge-new { background: #ff4757; color: white; }
.badge-confirmed { background: #2ed573; color: black; }
@media (max-width: 600px) {
.header-panel { flex-direction: column; align-items: stretch; text-align: center; }
.product-item { flex-direction: column; align-items: stretch; }
.product-info { width: 100%; }
.product-actions { align-self: flex-end; }
.form-row { flex-direction: column; }
}
</style>
</head>
<body>
<div class="container">
<div class="header-panel">
<h1><i class="fas fa-crown"></i> Админ-панель</h1>
<a href="/" class="btn btn-primary"><i class="fas fa-store"></i> В заведение</a>
</div>
<div class="card" style="padding: 0;">
<div class="category-header" onclick="toggleCategory('admin-settings')" style="border-radius: 16px; border-bottom: none;">
<div style="display: flex; align-items: center; gap: 10px;">
<i class="fas fa-chevron-down" id="icon-admin-settings" style="color: var(--text-muted);"></i>
<span style="font-weight: 800; font-size: 1.2rem; color: var(--text);"><i class="fas fa-cogs" style="color:var(--info);"></i> Настройки</span>
</div>
</div>
<div class="category-content" id="admin-settings" style="padding: 20px; border-top: 1px solid var(--border);">
<form method="POST" enctype="multipart/form-data" class="add-product-form" style="padding: 0;" onsubmit="showLoading(this)">
<input type="hidden" name="action" value="update_settings">
<div>
<label>Название заведения</label>
<input type="text" name="cafe_name" value="{{ settings.cafe_name }}" required>
</div>
<div>
<label>Логотип (загрузить файл)</label>
<div style="margin-bottom: 10px;">
<img src="{{ settings.logo_url }}" style="height: 60px; border-radius: 50%; object-fit: cover; border: 2px solid var(--border);">
</div>
<input type="file" name="logo_file" accept="image/*">
<div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 5px;">Оставьте пустым, чтобы не менять текущий логотип</div>
</div>
<div class="form-row">
<div>
<label>WhatsApp (Смена 1)</label>
<input type="text" name="wa_shift1" value="{{ settings.wa_shift1 }}" required>
</div>
<div>
<label>WhatsApp (Смена 2)</label>
<input type="text" name="wa_shift2" value="{{ settings.wa_shift2 }}" required>
</div>
</div>
<button type="submit" class="btn btn-success" style="color:#000; justify-content:center; margin-top:10px;"><i class="fas fa-save"></i> Сохранить настройки</button>
</form>
</div>
</div>
<div class="card" style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:15px;">
<div>
<h3 style="margin:0; color:var(--text);">Активная смена: <span style="color:var(--primary); font-size:1.3rem;">Смена {{ settings.active_shift }}</span></h3>
<div style="color:var(--text-muted); margin-top:5px; font-size:0.9rem;"><i class="fab fa-whatsapp"></i> Текущий номер: {{ settings.wa_shift1 if settings.active_shift == 1 else settings.wa_shift2 }}</div>
</div>
<form method="POST" style="margin:0;">
<input type="hidden" name="action" value="switch_shift">
<button type="submit" class="btn btn-warning"><i class="fas fa-exchange-alt"></i> Переключить смену</button>
</form>
</div>
<div class="sync-panel">
<form method="POST" action="/force_upload" onsubmit="showLoading(this)">
<button type="submit" class="btn btn-success" style="color:#000;"><i class="fas fa-cloud-upload-alt"></i> Сохранить на сервер</button>
</form>
<form method="POST" action="/force_download" onsubmit="showLoading(this)">
<button type="submit" class="btn btn-primary" style="color:#000;"><i class="fas fa-cloud-download-alt"></i> Скачать с сервера</button>
</form>
</div>
<div class="card" style="padding: 0;">
<div class="category-header" onclick="toggleCategory('active-orders')" style="border-radius: 16px; border-bottom: none; background: #1a1a1a;">
<div style="display: flex; align-items: center; gap: 10px;">
<i class="fas fa-chevron-down" id="icon-active-orders" style="color: var(--text-muted);"></i>
<span style="font-weight: 800; font-size: 1.2rem; color: var(--text);"><i class="fas fa-concierge-bell" style="color:var(--danger);"></i> Активные заказы</span>
</div>
</div>
<div class="category-content active" id="active-orders" style="padding: 0 20px 20px 20px; border-top: 1px solid var(--border);">
<div style="overflow-x: auto; padding-top: 15px;">
<table class="orders-table">
<tr>
<th>ID / Дата</th>
<th>Детали заказа</th>
<th>Сумма</th>
<th>Статус</th>
<th>Действия</th>
</tr>
{% for order in orders.values()|sort(attribute='created_at', reverse=True) %}
{% if order.status != 'confirmed' %}
<tr>
<td>
<a href="/order/{{ order.id }}" target="_blank" style="color:var(--info); font-weight:bold; text-decoration:none;">{{ order.id }}</a><br>
<span style="font-size:0.8rem; color:var(--text-muted);">{{ order.created_at }}</span>
</td>
<td style="font-size:0.9rem;">
{% if order.order_type == 'dine_in' %}
<span style="color:var(--primary); font-weight:bold;">В заведении</span><br>
Столик: {{ order.table_number }}
{% else %}
<span style="color:var(--primary); font-weight:bold;">Доставка</span><br>
{{ order.customer_name }}<br>
<i class="fas fa-phone" style="font-size:0.7rem; color:var(--text-muted);"></i> {{ order.customer_phone }}<br>
<i class="fas fa-map-marker-alt" style="font-size:0.7rem; color:var(--text-muted);"></i> {{ order.customer_address }}
{% endif %}
</td>
<td style="font-weight:800; color:var(--primary);">{{ order.total_price }} {{ currency_code }}</td>
<td><span class="badge badge-new">Новый</span></td>
<td style="display:flex; gap:5px; flex-direction:column;">
<form method="POST" style="margin:0;">
<input type="hidden" name="action" value="confirm_order">
<input type="hidden" name="order_id" value="{{ order.id }}">
<button type="submit" class="btn btn-success" style="width:100%; padding:6px; font-size:0.8rem; color:#000;"><i class="fas fa-check"></i> Подтвердить</button>
</form>
<form method="POST" style="margin:0;" onsubmit="return confirm('Точно удалить этот заказ?');">
<input type="hidden" name="action" value="delete_order">
<input type="hidden" name="order_id" value="{{ order.id }}">
<button type="submit" class="btn btn-danger" style="width:100%; padding:6px; font-size:0.8rem;"><i class="fas fa-trash"></i> Удалить</button>
</form>
<a href="/order/{{ order.id }}" target="_blank" class="btn btn-dark" style="width:100%; padding:6px; font-size:0.8rem; text-align:center; justify-content:center;"><i class="fas fa-file-invoice"></i> Чек</a>
</td>
</tr>
{% endif %}
{% endfor %}
</table>
</div>
</div>
</div>
<div class="card" style="padding: 0;">
<div class="category-header" onclick="toggleCategory('orders-history')" style="border-radius: 16px; border-bottom: none;">
<div style="display: flex; align-items: center; gap: 10px;">
<i class="fas fa-chevron-down" id="icon-orders-history" style="color: var(--text-muted);"></i>
<span style="font-weight: 800; font-size: 1.2rem; color: var(--text);"><i class="fas fa-history" style="color:var(--info);"></i> История заказов</span>
</div>
</div>
<div class="category-content" id="orders-history" style="padding: 0 20px 20px 20px; border-top: 1px solid var(--border);">
<div style="overflow-x: auto; padding-top: 15px;">
<table class="orders-table">
<tr>
<th>ID / Дата</th>
<th>Детали заказа</th>
<th>Сумма</th>
<th>Скидка</th>
<th>Статус</th>
<th>Действия</th>
</tr>
{% for order in orders.values()|sort(attribute='created_at', reverse=True) %}
{% if order.status == 'confirmed' %}
{% set raw_total = 0 %}
{% for item in order.cart %}
{% set raw_total = raw_total + (item.price|float * item.quantity|int) %}
{% endfor %}
<tr>
<td>
<a href="/order/{{ order.id }}" target="_blank" style="color:var(--info); font-weight:bold; text-decoration:none;">{{ order.id }}</a><br>
<span style="font-size:0.8rem; color:var(--text-muted);">{{ order.created_at }}</span>
</td>
<td style="font-size:0.9rem;">
{% if order.order_type == 'dine_in' %}
Столик: {{ order.table_number }}
{% else %}
{{ order.customer_name }}
{% endif %}
</td>
<td style="font-weight:600;">{{ raw_total }} {{ currency_code }}</td>
<td>
<form method="POST" style="display:flex; gap:5px; margin:0; align-items:center;">
<input type="hidden" name="action" value="apply_discount">
<input type="hidden" name="order_id" value="{{ order.id }}">
<input type="number" name="discount_amount" value="{{ order.discount|default(0) }}" min="0" step="0.01" style="width:80px; padding:6px; font-size:0.9rem;">
<button type="submit" class="btn btn-warning" style="padding:6px 10px;" title="Применить скидку"><i class="fas fa-check"></i></button>
</form>
</td>
<td><span class="badge badge-confirmed">Завершен</span></td>
<td>
<a href="/order/{{ order.id }}" class="btn btn-primary" style="padding:6px 10px;" target="_blank"><i class="fas fa-eye"></i></a>
<form method="POST" style="display:inline-block; margin:0;" onsubmit="return confirm('Удалить из истории?');">
<input type="hidden" name="action" value="delete_order">
<input type="hidden" name="order_id" value="{{ order.id }}">
<button type="submit" class="btn btn-danger" style="padding:6px 10px;"><i class="fas fa-trash"></i></button>
</form>
</td>
</tr>
{% endif %}
{% endfor %}
</table>
</div>
</div>
</div>
<div class="card">
<h2>Управление категориями</h2>
<form method="POST" class="add-cat-form">
<input type="hidden" name="action" value="add_category">
<input type="text" name="category_name" placeholder="Название новой категории" required autocomplete="off">
<input type="text" name="category_icon" placeholder="Класс иконки (напр. fas fa-pizza-slice)" value="fas fa-utensils" required autocomplete="off">
<button type="submit" class="btn btn-dark"><i class="fas fa-plus"></i> Добавить</button>
</form>
</div>
<div class="search-bar-admin">
<i class="fas fa-search"></i>
<input type="text" id="adminSearch" placeholder="Поиск по категориям и блюдам..." oninput="filterAdmin()">
</div>
{% for category in categories %}
<div class="category-block">
<div class="category-header" onclick="toggleCategory('cat-{{ loop.index }}')">
<div style="display: flex; align-items: center; gap: 10px;">
<i class="fas fa-chevron-down" id="icon-cat-{{ loop.index }}" style="color: var(--text-muted);"></i>
<span class="cat-title-text"><i class="{{ category.icon }}" style="color:var(--info); margin-right:5px;"></i> {{ category.name }}</span>
</div>
<form method="POST" style="margin:0;" onclick="event.stopPropagation();" onsubmit="return confirm('Удалить категорию и все ее блюда?');">
<input type="hidden" name="action" value="delete_category">
<input type="hidden" name="category_name" value="{{ category.name }}">
<button type="submit" class="btn btn-danger"><i class="fas fa-trash-alt"></i></button>
</form>
</div>
<div class="category-content" id="cat-{{ loop.index }}">
<div style="padding: 15px; border-bottom: 1px solid var(--border);">
<form method="POST" style="display: flex; gap: 10px; flex-wrap: wrap;">
<input type="hidden" name="action" value="edit_category">
<input type="hidden" name="old_name" value="{{ category.name }}">
<input type="text" name="new_name" value="{{ category.name }}" required style="flex: 1; min-width: 150px;">
<input type="text" name="new_icon" value="{{ category.icon }}" required style="flex: 1; min-width: 150px;">
<button type="submit" class="btn btn-primary" style="white-space: nowrap;"><i class="fas fa-save"></i> Обновить категорию</button>
</form>
</div>
<div class="toggle-add-product" onclick="toggleAddProduct('add-prod-{{ loop.index }}')">
<i class="fas fa-plus"></i> Добавить блюдо
</div>
<div class="add-product-wrapper" id="add-prod-{{ loop.index }}">
<form class="add-product-form" method="POST" enctype="multipart/form-data" onsubmit="showLoading(this)">
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="category" value="{{ category.name }}">
<div style="font-weight: 600; font-size: 0.9rem; color: var(--primary);">Новое блюдо в категории "{{ category.name }}"</div>
<div class="form-row">
<input type="text" name="name" placeholder="Название блюда" required autocomplete="off" style="flex:2;">
<input type="number" name="price" placeholder="Цена" required step="0.01" style="flex:1;">
</div>
<textarea name="description" placeholder="Описание блюда (необязательно)"></textarea>
<div class="file-input-wrapper">
<input type="file" name="photos" accept="image/*" multiple max="10">
<div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 5px;">Можно выбрать до 10 фото (необязательно)</div>
</div>
<button type="submit" class="btn btn-success" style="width: 100%; justify-content: center; color:#000;"><i class="fas fa-check"></i> Сохранить блюдо</button>
</form>
</div>
{% for product in products %}
{% if product.category == category.name %}
<div class="product-item">
<div class="product-info">
{% if product.photos and product.photos|length > 0 %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photos[0] }}" class="product-img">
{% else %}
<div class="product-img" style="display:flex;align-items:center;justify-content:center;color:#333;"><i class="fas fa-image"></i></div>
{% endif %}
<div class="product-details">
<span class="product-name">{{ product.name }}</span>
{% if product.description %}
<span class="product-desc">{{ product.description[:50] }}{{ '...' if product.description|length > 50 else '' }}</span>
{% endif %}
<span class="product-meta">{{ product.price }} {{ currency_code }} • Фото: {{ product.photos|length if product.photos else 0 }}/10</span>
</div>
</div>
<div class="product-actions">
<button class="btn btn-warning" onclick="toggleEditProduct('edit-prod-{{ product.product_id }}')"><i class="fas fa-edit"></i></button>
<form method="POST" style="margin:0;" onsubmit="return confirm('Удалить блюдо?');">
<input type="hidden" name="action" value="delete_product">
<input type="hidden" name="product_id" value="{{ product.product_id }}">
<button type="submit" class="btn btn-danger"><i class="fas fa-times"></i></button>
</form>
</div>
<div class="add-product-wrapper" id="edit-prod-{{ product.product_id }}" style="width: 100%; margin-top: 15px; border-top: 1px dashed var(--border); padding-top: 15px;">
<form class="add-product-form" method="POST" enctype="multipart/form-data" onsubmit="showLoading(this)" style="padding: 0;">
<input type="hidden" name="action" value="edit_product">
<input type="hidden" name="product_id" value="{{ product.product_id }}">
<input type="hidden" name="category" value="{{ category.name }}">
<div style="font-weight: 600; font-size: 0.9rem; color: var(--primary);">Редактирование блюда</div>
<div class="form-row">
<input type="text" name="name" value="{{ product.name }}" required autocomplete="off" style="flex:2;">
<input type="number" name="price" value="{{ product.price }}" required step="0.01" style="flex:1;">
</div>
<textarea name="description">{{ product.description }}</textarea>
<div class="file-input-wrapper">
<input type="file" name="photos" accept="image/*" multiple max="10">
<div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 5px;">Оставьте пустым, чтобы не менять фото</div>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%; justify-content: center;"><i class="fas fa-save"></i> Сохранить изменения</button>
</form>
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<script>
function showLoading(form) {
const btn = form.querySelector('button[type="submit"]');
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Загрузка...';
btn.style.pointerEvents = 'none';
btn.style.opacity = '0.7';
}
function toggleCategory(id) {
const content = document.getElementById(id);
const icon = document.getElementById('icon-' + id);
if(content.classList.contains('active')) {
content.classList.remove('active');
if(icon) { icon.classList.remove('fa-chevron-up'); icon.classList.add('fa-chevron-down'); }
} else {
content.classList.add('active');
if(icon) { icon.classList.remove('fa-chevron-down'); icon.classList.add('fa-chevron-up'); }
}
}
function toggleAddProduct(id) {
const form = document.getElementById(id);
form.classList.toggle('active');
}
function toggleEditProduct(id) {
const form = document.getElementById(id);
form.classList.toggle('active');
}
function filterAdmin() {
const query = document.getElementById('adminSearch').value.toLowerCase();
const categories = document.querySelectorAll('.category-block');
categories.forEach(cat => {
const catNameEl = cat.querySelector('.cat-title-text');
if(!catNameEl) return;
const catName = catNameEl.innerText.toLowerCase();
const products = cat.querySelectorAll('.product-item');
let catMatch = catName.includes(query);
let hasVisibleProduct = false;
products.forEach(prod => {
const prodName = prod.querySelector('.product-name').innerText.toLowerCase();
if (prodName.includes(query) || catMatch) {
prod.style.display = 'flex';
hasVisibleProduct = true;
} else {
prod.style.display = 'none';
}
});
if (catMatch || hasVisibleProduct) {
cat.style.display = 'block';
if (query && hasVisibleProduct) {
cat.querySelector('.category-content').classList.add('active');
const icon = cat.querySelector('.fas.fa-chevron-down, .fas.fa-chevron-up');
if(icon) icon.className = 'fas fa-chevron-up';
}
} else {
cat.style.display = 'none';
}
if (!query) {
cat.querySelector('.category-content').classList.remove('active');
const icon = cat.querySelector('.fas.fa-chevron-up, .fas.fa-chevron-down');
if(icon) icon.className = 'fas fa-chevron-down';
}
});
}
</script>
</body>
</html>
'''
@app.route('/')
def catalog():
data = load_data()
all_products = data.get('products', [])
categories = data.get('categories', [])
settings = data.get('settings', {})
return render_template_string(
CATALOG_TEMPLATE,
products_json=json.dumps(all_products),
categories_json=json.dumps(categories),
repo_id=REPO_ID,
currency_code=CURRENCY_CODE,
settings=settings
)
@app.route('/create_order', methods=['POST'])
def create_order():
order_data = request.get_json()
if not order_data or 'cart' not in order_data:
return jsonify({"error": "Bad request"}), 400
cart_items = order_data['cart']
total_price = sum(float(item['price']) * int(item['quantity']) for item in cart_items)
order_type = order_data.get('order_type', 'delivery')
customer_name = order_data.get('customer_name', 'Не указано')
customer_phone = order_data.get('customer_phone', 'Не указано')
customer_address = order_data.get('customer_address', 'Не указано')
table_number = order_data.get('table_number', 'Не указано')
processed_cart = []
for item in cart_items:
processed_cart.append({
"product_id": item.get('product_id'),
"name": item['name'],
"price": float(item['price']),
"quantity": int(item['quantity']),
"photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photos'][0]}" if item.get('photos') else "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjMWExYTFhIi8+PC9zdmc+"
})
order_id = f"HK-{datetime.now().strftime('%Y%m%d')}-{str(len(load_data().get('orders', {}))+1).zfill(3)}"
new_order = {
"id": order_id,
"created_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
"cart": processed_cart,
"discount": 0,
"total_price": total_price,
"order_type": order_type,
"customer_name": customer_name,
"customer_phone": customer_phone,
"customer_address": customer_address,
"table_number": table_number,
"status": "new"
}
data = load_data()
data['orders'][order_id] = new_order
save_data(data)
return jsonify({"order_id": order_id}), 201
@app.route('/order/<order_id>')
def view_order(order_id):
data = load_data()
order = data.get('orders', {}).get(order_id)
settings = data.get('settings', {})
if not order:
return "Order not found", 404
return render_template_string(
ORDER_TEMPLATE,
order=order,
currency_code=CURRENCY_CODE,
settings=settings
)
@app.route('/edit_order/<order_id>', methods=['POST'])
def edit_order(order_id):
data = load_data()
order = data.get('orders', {}).get(order_id)
if not order:
return jsonify({"success": False, "error": "Order not found"}), 404
req_data = request.get_json()
product_id = req_data.get('product_id')
change = req_data.get('change', 0)
exact_qty = req_data.get('exact_qty')
remove = req_data.get('remove', False)
for item in order['cart']:
if item.get('product_id') == product_id:
if remove:
order['cart'].remove(item)
else:
if exact_qty is not None:
item['quantity'] = int(exact_qty)
else:
item['quantity'] += change
if item['quantity'] <= 0:
order['cart'].remove(item)
break
cart_total = sum(float(i['price']) * int(i['quantity']) for i in order['cart'])
discount = order.get('discount', 0)
order['total_price'] = max(0, cart_total - discount)
save_data(data)
return jsonify({"success": True, "total_price": order['total_price']})
@app.route('/admin', methods=['GET', 'POST'])
def admin():
data = load_data()
products = data.get('products', [])
categories = data.get('categories', [])
orders = data.get('orders', {})
settings = data.get('settings', {})
if request.method == 'POST':
action = request.form.get('action')
if action == 'update_settings':
settings['cafe_name'] = request.form.get('cafe_name', '').strip()
settings['wa_shift1'] = request.form.get('wa_shift1', '').strip()
settings['wa_shift2'] = request.form.get('wa_shift2', '').strip()
logo_file = request.files.get('logo_file')
if logo_file and logo_file.filename and HF_TOKEN_WRITE:
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
api = HfApi()
ext = os.path.splitext(logo_file.filename)[1].lower()
if ext in ['.jpg', '.jpeg', '.png', '.webp', '.gif']:
photo_filename = f"logo_{uuid4().hex}{ext}"
temp_path = os.path.join(uploads_dir, photo_filename)
logo_file.save(temp_path)
try:
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=f"photos/{photo_filename}",
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE
)
settings['logo_url'] = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{photo_filename}"
except Exception:
pass
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
data['settings'] = settings
save_data(data)
elif action == 'switch_shift':
current = settings.get('active_shift', 1)
settings['active_shift'] = 2 if current == 1 else 1
data['settings'] = settings
save_data(data)
elif action == 'confirm_order':
order_id = request.form.get('order_id')
if order_id in orders:
orders[order_id]['status'] = 'confirmed'
data['orders'] = orders
save_data(data)
elif action == 'delete_order':
order_id = request.form.get('order_id')
if order_id in orders:
del orders[order_id]
data['orders'] = orders
save_data(data)
elif action == 'apply_discount':
order_id = request.form.get('order_id')
discount_val = float(request.form.get('discount_amount', 0))
if order_id in orders:
order = orders[order_id]
order['discount'] = discount_val
cart_total = sum(float(i['price']) * int(i['quantity']) for i in order['cart'])
order['total_price'] = max(0, cart_total - discount_val)
data['orders'] = orders
save_data(data)
elif action == 'add_category':
cat_name = request.form.get('category_name', '').strip()
cat_icon = request.form.get('category_icon', 'fas fa-utensils').strip()
if cat_name and not any(c.get('name') == cat_name for c in categories):
categories.append({'name': cat_name, 'icon': cat_icon})
data['categories'] = categories
save_data(data)
elif action == 'edit_category':
old_name = request.form.get('old_name')
new_name = request.form.get('new_name', '').strip()
new_icon = request.form.get('new_icon', 'fas fa-utensils').strip()
if new_name:
for c in categories:
if c.get('name') == old_name:
c['name'] = new_name
c['icon'] = new_icon
break
for p in products:
if p.get('category') == old_name:
p['category'] = new_name
data['categories'] = categories
data['products'] = products
save_data(data)
elif action == 'delete_category':
cat_name = request.form.get('category_name')
data['categories'] = [c for c in categories if c.get('name') != cat_name]
data['products'] = [p for p in products if p.get('category') != cat_name]
save_data(data)
elif action == 'add_product':
name = request.form.get('name', '').strip()
price = float(request.form.get('price', 0))
description = request.form.get('description', '').strip()
category = request.form.get('category')
uploaded_photos = request.files.getlist('photos')[:10]
photos_list = []
if uploaded_photos and HF_TOKEN_WRITE:
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
api = HfApi()
for photo in uploaded_photos:
if photo and photo.filename:
ext = os.path.splitext(photo.filename)[1].lower()
if ext not in ['.jpg', '.jpeg', '.png', '.webp', '.gif']:
continue
photo_filename = f"{uuid4().hex}{ext}"
temp_path = os.path.join(uploads_dir, photo_filename)
photo.save(temp_path)
try:
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=f"photos/{photo_filename}",
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE
)
photos_list.append(photo_filename)
except Exception:
pass
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
new_product = {
'product_id': uuid4().hex,
'name': name,
'price': price,
'description': description,
'category': category,
'photos': photos_list
}
products.append(new_product)
data['products'] = products
save_data(data)
elif action == 'edit_product':
pid = request.form.get('product_id')
name = request.form.get('name', '').strip()
price = float(request.form.get('price', 0))
description = request.form.get('description', '').strip()
uploaded_photos = request.files.getlist('photos')[:10]
photos_list = []
if uploaded_photos and uploaded_photos[0].filename and HF_TOKEN_WRITE:
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
api = HfApi()
for photo in uploaded_photos:
if photo and photo.filename:
ext = os.path.splitext(photo.filename)[1].lower()
if ext not in ['.jpg', '.jpeg', '.png', '.webp', '.gif']:
continue
photo_filename = f"{uuid4().hex}{ext}"
temp_path = os.path.join(uploads_dir, photo_filename)
photo.save(temp_path)
try:
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=f"photos/{photo_filename}",
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE
)
photos_list.append(photo_filename)
except Exception:
pass
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
for p in products:
if p.get('product_id') == pid:
p['name'] = name
p['price'] = price
p['description'] = description
if photos_list:
p['photos'] = photos_list
break
data['products'] = products
save_data(data)
elif action == 'delete_product':
pid = request.form.get('product_id')
data['products'] = [p for p in products if p.get('product_id') != pid]
save_data(data)
return redirect(url_for('admin'))
return render_template_string(
ADMIN_TEMPLATE,
products=products,
categories=categories,
orders=orders,
repo_id=REPO_ID,
currency_code=CURRENCY_CODE,
settings=settings
)
@app.route('/force_upload', methods=['POST'])
def force_upload():
upload_db_to_hf()
return redirect(url_for('admin'))
@app.route('/force_download', methods=['POST'])
def force_download():
download_db_from_hf()
return redirect(url_for('admin'))
if __name__ == '__main__':
download_db_from_hf()
load_data()
if HF_TOKEN_WRITE:
threading.Thread(target=periodic_backup, daemon=True).start()
port = int(os.environ.get('PORT', 7860))
app.run(host='0.0.0.0', port=port)