Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
import os
|
| 4 |
import io
|
| 5 |
import base64
|
|
@@ -37,6 +35,8 @@ DOWNLOAD_RETRIES = 3
|
|
| 37 |
DOWNLOAD_DELAY = 5
|
| 38 |
ALMATY_TZ = timezone(timedelta(hours=6))
|
| 39 |
|
|
|
|
|
|
|
| 40 |
CURRENCIES = {
|
| 41 |
'KGS': 'Кыргызский сом',
|
| 42 |
'KZT': 'Казахстанский тенге',
|
|
@@ -154,112 +154,116 @@ def periodic_backup():
|
|
| 154 |
upload_db_to_hf()
|
| 155 |
|
| 156 |
def load_data():
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
| 168 |
data = {}
|
| 169 |
-
|
| 170 |
data = {}
|
| 171 |
-
|
| 172 |
-
data = {}
|
| 173 |
-
return data
|
| 174 |
|
| 175 |
def save_data(data):
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
|
|
|
| 182 |
|
| 183 |
def get_env_data(env_id):
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
env_data = all_data.get(env_id, {})
|
| 211 |
-
if not env_data:
|
| 212 |
-
env_data = {
|
| 213 |
-
'products': [], 'categories':[], 'orders': {}, 'employees':[], 'blocks':[],
|
| 214 |
-
'organization_info': default_organization_info,
|
| 215 |
-
'settings': default_settings,
|
| 216 |
-
'inventory_history':[]
|
| 217 |
}
|
| 218 |
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
if
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
-
|
| 255 |
-
|
| 256 |
|
| 257 |
-
|
| 258 |
|
| 259 |
def save_env_data(env_id, env_data):
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
|
|
|
| 263 |
|
| 264 |
def configure_gemini():
|
| 265 |
if not GOOGLE_API_KEY:
|
|
@@ -435,7 +439,7 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 435 |
<div class="env-actions">
|
| 436 |
<a href="{{ url_for('admin', env_id=env.id) }}" class="button" target="_blank"><i class="fas fa-tools"></i> Админ</a>
|
| 437 |
<a href="{{ url_for('catalog', env_id=env.id) }}" class="button" target="_blank"><i class="fas fa-store"></i> Каталог</a>
|
| 438 |
-
<form method="POST" action="{{ url_for('delete_environment', env_id=env.id) }}" style="display:inline;" onsubmit="
|
| 439 |
<button type="submit" class="button delete-button"><i class="fas fa-trash-alt"></i></button>
|
| 440 |
</form>
|
| 441 |
</div>
|
|
@@ -944,13 +948,12 @@ CATALOG_TEMPLATE = '''
|
|
| 944 |
}
|
| 945 |
|
| 946 |
function getProductById(productId) { return allProducts.find(p => p.product_id === productId); }
|
| 947 |
-
function getProductIndexById(productId) { return allProducts.findIndex(p => p.product_id === productId); }
|
| 948 |
|
| 949 |
function openModalById(productId) {
|
| 950 |
-
const
|
| 951 |
-
if (
|
| 952 |
fetch(`/${envId}/track_view/${productId}`, {method: 'POST'}).catch(e=>{});
|
| 953 |
-
loadProductDetails(
|
| 954 |
const modal = document.getElementById('productModal');
|
| 955 |
if (modal) { modal.style.display = "block"; document.body.style.overflow = 'hidden'; }
|
| 956 |
}
|
|
@@ -961,11 +964,11 @@ CATALOG_TEMPLATE = '''
|
|
| 961 |
if (!document.querySelector('.modal[style*="display: block"]')) { document.body.style.overflow = 'auto'; }
|
| 962 |
}
|
| 963 |
|
| 964 |
-
function loadProductDetails(
|
| 965 |
const modalContent = document.getElementById('modalContent');
|
| 966 |
if (!modalContent) return;
|
| 967 |
modalContent.innerHTML = '<p style="text-align:center; padding: 40px; font-weight: 600;">Загрузка данных...</p>';
|
| 968 |
-
fetch(`/${envId}/product/${
|
| 969 |
.then(response => {
|
| 970 |
if (!response.ok) throw new Error(`Ошибка ${response.status}`);
|
| 971 |
return response.text();
|
|
@@ -1619,7 +1622,7 @@ HISTORY_TEMPLATE = '''
|
|
| 1619 |
<td>{{ "%.2f"|format(order.total_price) }} {{ currency_code }}</td>
|
| 1620 |
<td>
|
| 1621 |
<a href="{{ url_for('view_order', env_id=env_id, order_id=order.id) }}" style="color: var(--bg-medium); margin-right: 15px; padding: 10px; display: inline-block;"><i class="fas fa-eye fa-lg"></i></a>
|
| 1622 |
-
<form method="POST" action="{{ url_for('delete_order', env_id=env_id, order_id=order.id) }}" style="display:inline;" onsubmit="
|
| 1623 |
<button type="submit" class="delete-btn"><i class="fas fa-trash-alt"></i></button>
|
| 1624 |
</form>
|
| 1625 |
</td>
|
|
@@ -2575,7 +2578,7 @@ ADMIN_TEMPLATE = '''
|
|
| 2575 |
{% if settings.env_mode == '2in1' %}
|
| 2576 |
<a href="{{ url_for('pos_page', env_id=env_id) }}?emp={{ emp.id }}" class="button" style="background-color: #28a745; font-size: 0.9rem;" target="_blank"><i class="fas fa-desktop"></i> Ссылка на кассу</a>
|
| 2577 |
{% endif %}
|
| 2578 |
-
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page, q=search_q) }}" style="margin: 0;" onsubmit="
|
| 2579 |
<input type="hidden" name="action" value="delete_employee">
|
| 2580 |
<input type="hidden" name="emp_id" value="{{ emp.id }}">
|
| 2581 |
<button type="submit" class="delete-button" style="margin: 0;"><i class="fas fa-trash-alt"></i></button>
|
|
@@ -2634,7 +2637,7 @@ ADMIN_TEMPLATE = '''
|
|
| 2634 |
<input type="hidden" name="block_id" value="{{ block.id }}">
|
| 2635 |
<button type="submit" class="button btn-small" style="background: #6c757d;" {% if loop.last %}disabled{% endif %}><i class="fas fa-arrow-down"></i></button>
|
| 2636 |
</form>
|
| 2637 |
-
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page, q=search_q) }}" style="margin: 0;" onsubmit="
|
| 2638 |
<input type="hidden" name="action" value="delete_block">
|
| 2639 |
<input type="hidden" name="block_id" value="{{ block.id }}">
|
| 2640 |
<button type="submit" class="button delete-button btn-small"><i class="fas fa-trash-alt"></i></button>
|
|
@@ -2750,7 +2753,7 @@ ADMIN_TEMPLATE = '''
|
|
| 2750 |
{% for category in categories %}
|
| 2751 |
<div class="item" style="display: flex; justify-content: space-between; align-items: center;">
|
| 2752 |
<span style="font-size: 1.05rem; font-weight: 500;">{{ category }}</span>
|
| 2753 |
-
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page, q=search_q) }}" style="margin: 0;" onsubmit="
|
| 2754 |
<input type="hidden" name="action" value="delete_category">
|
| 2755 |
<input type="hidden" name="category_name" value="{{ category }}">
|
| 2756 |
<button type="submit" class="delete-button" style="margin: 0;"><i class="fas fa-trash-alt"></i></button>
|
|
@@ -2889,15 +2892,15 @@ ADMIN_TEMPLATE = '''
|
|
| 2889 |
</div>
|
| 2890 |
|
| 2891 |
<div class="item-actions">
|
| 2892 |
-
<button type="button" class="button" onclick="toggleEditForm('edit-form-{{
|
| 2893 |
-
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page, q=search_q) }}" style="margin:0;" onsubmit="
|
| 2894 |
<input type="hidden" name="action" value="delete_product">
|
| 2895 |
<input type="hidden" name="product_id" value="{{ product.get('product_id', '') }}">
|
| 2896 |
<button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
|
| 2897 |
</form>
|
| 2898 |
</div>
|
| 2899 |
|
| 2900 |
-
<div id="edit-form-{{
|
| 2901 |
<h4 style="margin-top: 0; font-size: 1.1rem;"><i class="fas fa-edit"></i> Редактирование</h4>
|
| 2902 |
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page, q=search_q) }}" enctype="multipart/form-data" onsubmit="showLoadingOverlay()">
|
| 2903 |
<input type="hidden" name="action" value="edit_product">
|
|
@@ -2906,21 +2909,21 @@ ADMIN_TEMPLATE = '''
|
|
| 2906 |
<input type="text" name="name" value="{{ product['name'] }}" required>
|
| 2907 |
|
| 2908 |
<label>Заменить фотографии (выбор новых удалит старые):</label>
|
| 2909 |
-
<input type="file" id="edit_photos_{{
|
| 2910 |
|
| 2911 |
<div style="margin-top: 20px; border: 1px solid #ccc; padding: 15px; border-radius: 8px; background: #fafafa;">
|
| 2912 |
<h4 style="margin-top: 0;"><i class="fas fa-tags"></i> Отметки товаров на фото</h4>
|
| 2913 |
-
<div id="thumbs-edit_{{
|
| 2914 |
-
<input type="hidden" name="tags_json" id="tags_json_edit_{{
|
| 2915 |
-
<div id="tagging-container-edit_{{
|
| 2916 |
-
<img id="tagging-img-edit_{{
|
| 2917 |
-
<div id="tag-markers-edit_{{
|
| 2918 |
</div>
|
| 2919 |
-
<div id="tags-list-edit_{{
|
| 2920 |
</div>
|
| 2921 |
|
| 2922 |
<label>Описание:</label>
|
| 2923 |
-
<textarea id="edit_description_{{
|
| 2924 |
|
| 2925 |
<label>Категория:</label>
|
| 2926 |
<select name="category">
|
|
@@ -2931,12 +2934,12 @@ ADMIN_TEMPLATE = '''
|
|
| 2931 |
</select>
|
| 2932 |
|
| 2933 |
<div style="margin-top: 20px; display: flex; align-items: center; gap: 8px;">
|
| 2934 |
-
<input type="checkbox" id="edit_in_stock_{{
|
| 2935 |
-
<label for="edit_in_stock_{{
|
| 2936 |
</div>
|
| 2937 |
<div style="margin-top: 10px; display: flex; align-items: center; gap: 8px;">
|
| 2938 |
-
<input type="checkbox" id="edit_is_top_{{
|
| 2939 |
-
<label for="edit_is_top_{{
|
| 2940 |
</div>
|
| 2941 |
<br>
|
| 2942 |
<button type="submit" class="add-button" style="margin-top: 25px;"><i class="fas fa-save"></i> Сохранить</button>
|
|
@@ -3034,14 +3037,14 @@ ADMIN_TEMPLATE = '''
|
|
| 3034 |
if(!formStates[scope]) formStates[scope] = { tags:[], fileUrls:[], currentIdx: 0, isEdit: scope.startsWith('edit_') };
|
| 3035 |
}
|
| 3036 |
|
| 3037 |
-
function toggleEditForm(formId,
|
| 3038 |
const formContainer = document.getElementById(formId);
|
| 3039 |
if (formContainer) {
|
| 3040 |
const isOpening = formContainer.style.display === 'none' || formContainer.style.display === '';
|
| 3041 |
formContainer.style.display = isOpening ? 'block' : 'none';
|
| 3042 |
if (isOpening) {
|
| 3043 |
-
const scope = `edit_${
|
| 3044 |
-
const product = allProductsForAdmin
|
| 3045 |
initScope(scope);
|
| 3046 |
|
| 3047 |
let tags =[];
|
|
@@ -3278,14 +3281,11 @@ ADMIN_TEMPLATE = '''
|
|
| 3278 |
}
|
| 3279 |
|
| 3280 |
window.openAdminPost = function(pid) {
|
| 3281 |
-
const
|
| 3282 |
-
if (
|
| 3283 |
-
|
| 3284 |
-
if (el && (el.style.display === 'none' || el.style.display === '')) {
|
| 3285 |
-
toggleEditForm(`edit-form-${index}`, index);
|
| 3286 |
-
}
|
| 3287 |
-
if (el) el.scrollIntoView({behavior: 'smooth', block: 'center'});
|
| 3288 |
}
|
|
|
|
| 3289 |
};
|
| 3290 |
|
| 3291 |
async function sendAdminAi(predefinedText) {
|
|
@@ -3318,7 +3318,6 @@ ADMIN_TEMPLATE = '''
|
|
| 3318 |
</body>
|
| 3319 |
</html>
|
| 3320 |
'''
|
| 3321 |
-
|
| 3322 |
@app.route('/')
|
| 3323 |
def index():
|
| 3324 |
return render_template_string(LANDING_PAGE_TEMPLATE)
|
|
@@ -3350,7 +3349,7 @@ def create_environment():
|
|
| 3350 |
if new_id not in all_data:
|
| 3351 |
break
|
| 3352 |
all_data[new_id] = {
|
| 3353 |
-
'products': [], 'categories':
|
| 3354 |
'organization_info': {
|
| 3355 |
"about_us": "Мы — Gippo312, ваш надежный партнер в мире уникальных товаров.",
|
| 3356 |
"shipping": "Доставка осуществляется по всему Кыргызстану.",
|
|
@@ -3700,8 +3699,8 @@ def track_view(env_id, product_id):
|
|
| 3700 |
save_env_data(env_id, data)
|
| 3701 |
return jsonify({"status": "ok"})
|
| 3702 |
|
| 3703 |
-
@app.route('/<env_id>/product/<
|
| 3704 |
-
def product_detail(env_id,
|
| 3705 |
data = get_env_data(env_id)
|
| 3706 |
all_products_raw = data.get('products',[])
|
| 3707 |
settings = data.get('settings', {})
|
|
@@ -3722,10 +3721,11 @@ def product_detail(env_id, index):
|
|
| 3722 |
products_in_stock.append(p)
|
| 3723 |
|
| 3724 |
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
|
| 3725 |
-
|
| 3726 |
-
|
| 3727 |
-
|
| 3728 |
return "Товар не найден или отсутствует в наличии.", 404
|
|
|
|
| 3729 |
return render_template_string(
|
| 3730 |
PRODUCT_DETAIL_TEMPLATE, product=product, repo_id=REPO_ID,
|
| 3731 |
currency_code=settings.get('currency_code', 'KGS'), settings=settings, env_id=env_id
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import io
|
| 3 |
import base64
|
|
|
|
| 35 |
DOWNLOAD_DELAY = 5
|
| 36 |
ALMATY_TZ = timezone(timedelta(hours=6))
|
| 37 |
|
| 38 |
+
db_lock = threading.RLock()
|
| 39 |
+
|
| 40 |
CURRENCIES = {
|
| 41 |
'KGS': 'Кыргызский сом',
|
| 42 |
'KZT': 'Казахстанский тенге',
|
|
|
|
| 154 |
upload_db_to_hf()
|
| 155 |
|
| 156 |
def load_data():
|
| 157 |
+
with db_lock:
|
| 158 |
+
try:
|
| 159 |
+
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 160 |
+
data = json.load(f)
|
| 161 |
+
if not isinstance(data, dict):
|
| 162 |
+
data = {}
|
| 163 |
+
except (FileNotFoundError, json.JSONDecodeError):
|
| 164 |
+
if download_db_from_hf(specific_file=DATA_FILE):
|
| 165 |
+
try:
|
| 166 |
+
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 167 |
+
data = json.load(f)
|
| 168 |
+
if not isinstance(data, dict):
|
| 169 |
+
data = {}
|
| 170 |
+
except (FileNotFoundError, json.JSONDecodeError):
|
| 171 |
data = {}
|
| 172 |
+
else:
|
| 173 |
data = {}
|
| 174 |
+
return data
|
|
|
|
|
|
|
| 175 |
|
| 176 |
def save_data(data):
|
| 177 |
+
with db_lock:
|
| 178 |
+
try:
|
| 179 |
+
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 180 |
+
json.dump(data, file, ensure_ascii=False, indent=4)
|
| 181 |
+
except Exception:
|
| 182 |
+
pass
|
| 183 |
+
upload_db_to_hf(specific_file=DATA_FILE)
|
| 184 |
|
| 185 |
def get_env_data(env_id):
|
| 186 |
+
with db_lock:
|
| 187 |
+
all_data = load_data()
|
| 188 |
+
default_organization_info = {
|
| 189 |
+
"about_us": "Мы — надежный партнер в мире уникальных товаров.",
|
| 190 |
+
"shipping": "Доставка осуществляется по всему Кыргызстану.",
|
| 191 |
+
"returns": "Возврат и обмен товара возможен в течение 14 дней.",
|
| 192 |
+
"contact": "Наш магазин находится по адресу: Рынок Кербен. Мы работаем ежедневно с 9:00 до 18:00."
|
| 193 |
+
}
|
| 194 |
+
default_settings = {
|
| 195 |
+
"organization_name": "Gippo312",
|
| 196 |
+
"whatsapp_number": "+996701202013",
|
| 197 |
+
"currency_code": "KGS",
|
| 198 |
+
"chat_name": "EVA",
|
| 199 |
+
"chat_avatar": None,
|
| 200 |
+
"color_scheme": "default",
|
| 201 |
+
"business_type": "retail",
|
| 202 |
+
"env_mode": "external",
|
| 203 |
+
"welcome_message_enabled": False,
|
| 204 |
+
"welcome_message_text": "Добро пожаловать в наш магазин!",
|
| 205 |
+
"inventory_tracking": False,
|
| 206 |
+
"admin_password_enabled": False,
|
| 207 |
+
"admin_password": "",
|
| 208 |
+
"checkout_fields_enabled": False,
|
| 209 |
+
"checkout_fields": {"name": False, "phone": False, "city": False, "address": False, "zip": False},
|
| 210 |
+
"categories_as_lines": False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
}
|
| 212 |
|
| 213 |
+
env_data = all_data.get(env_id, {})
|
| 214 |
+
if not env_data:
|
| 215 |
+
env_data = {
|
| 216 |
+
'products': [], 'categories':[], 'orders': {}, 'employees':[], 'blocks':[],
|
| 217 |
+
'organization_info': default_organization_info,
|
| 218 |
+
'settings': default_settings,
|
| 219 |
+
'inventory_history':[]
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
if 'products' not in env_data: env_data['products'] =[]
|
| 223 |
+
if 'categories' not in env_data: env_data['categories'] =[]
|
| 224 |
+
if 'orders' not in env_data: env_data['orders'] = {}
|
| 225 |
+
if 'organization_info' not in env_data: env_data['organization_info'] = default_organization_info
|
| 226 |
+
if 'settings' not in env_data: env_data['settings'] = default_settings
|
| 227 |
+
if 'employees' not in env_data: env_data['employees'] =[]
|
| 228 |
+
if 'blocks' not in env_data: env_data['blocks'] =[]
|
| 229 |
+
if 'inventory_history' not in env_data: env_data['inventory_history'] =[]
|
| 230 |
+
|
| 231 |
+
settings_changed = False
|
| 232 |
+
for key, value in default_settings.items():
|
| 233 |
+
if key not in env_data['settings']:
|
| 234 |
+
env_data['settings'][key] = value
|
| 235 |
+
settings_changed = True
|
| 236 |
+
|
| 237 |
+
products_changed = False
|
| 238 |
+
for product in env_data['products']:
|
| 239 |
+
if 'product_id' not in product:
|
| 240 |
+
product['product_id'] = uuid4().hex
|
| 241 |
+
products_changed = True
|
| 242 |
+
if 'views' not in product:
|
| 243 |
+
product['views'] = 0
|
| 244 |
+
products_changed = True
|
| 245 |
+
if 'tags' not in product:
|
| 246 |
+
product['tags'] =[]
|
| 247 |
+
products_changed = True
|
| 248 |
+
else:
|
| 249 |
+
for tag in product['tags']:
|
| 250 |
+
if 'stock' not in tag:
|
| 251 |
+
tag['stock'] = 0
|
| 252 |
+
products_changed = True
|
| 253 |
+
if 'stock_batches' not in tag:
|
| 254 |
+
tag['stock_batches'] = [{"qty": tag['stock'], "price": tag.get('price', 0), "box_price": tag.get('box_price', 0)}]
|
| 255 |
+
products_changed = True
|
| 256 |
|
| 257 |
+
if products_changed or settings_changed:
|
| 258 |
+
save_env_data(env_id, env_data)
|
| 259 |
|
| 260 |
+
return env_data
|
| 261 |
|
| 262 |
def save_env_data(env_id, env_data):
|
| 263 |
+
with db_lock:
|
| 264 |
+
all_data = load_data()
|
| 265 |
+
all_data[env_id] = env_data
|
| 266 |
+
save_data(all_data)
|
| 267 |
|
| 268 |
def configure_gemini():
|
| 269 |
if not GOOGLE_API_KEY:
|
|
|
|
| 439 |
<div class="env-actions">
|
| 440 |
<a href="{{ url_for('admin', env_id=env.id) }}" class="button" target="_blank"><i class="fas fa-tools"></i> Админ</a>
|
| 441 |
<a href="{{ url_for('catalog', env_id=env.id) }}" class="button" target="_blank"><i class="fas fa-store"></i> Каталог</a>
|
| 442 |
+
<form method="POST" action="{{ url_for('delete_environment', env_id=env.id) }}" style="display:inline;" onsubmit="if(!confirm('Вы уверены, что хотите удалить среду {{ env.id }}? Это действие необратимо.')) return false;">
|
| 443 |
<button type="submit" class="button delete-button"><i class="fas fa-trash-alt"></i></button>
|
| 444 |
</form>
|
| 445 |
</div>
|
|
|
|
| 948 |
}
|
| 949 |
|
| 950 |
function getProductById(productId) { return allProducts.find(p => p.product_id === productId); }
|
|
|
|
| 951 |
|
| 952 |
function openModalById(productId) {
|
| 953 |
+
const product = getProductById(productId);
|
| 954 |
+
if (!product) { alert("Ошибка: товар не найден."); return; }
|
| 955 |
fetch(`/${envId}/track_view/${productId}`, {method: 'POST'}).catch(e=>{});
|
| 956 |
+
loadProductDetails(productId);
|
| 957 |
const modal = document.getElementById('productModal');
|
| 958 |
if (modal) { modal.style.display = "block"; document.body.style.overflow = 'hidden'; }
|
| 959 |
}
|
|
|
|
| 964 |
if (!document.querySelector('.modal[style*="display: block"]')) { document.body.style.overflow = 'auto'; }
|
| 965 |
}
|
| 966 |
|
| 967 |
+
function loadProductDetails(productId) {
|
| 968 |
const modalContent = document.getElementById('modalContent');
|
| 969 |
if (!modalContent) return;
|
| 970 |
modalContent.innerHTML = '<p style="text-align:center; padding: 40px; font-weight: 600;">Загрузка данных...</p>';
|
| 971 |
+
fetch(`/${envId}/product/${productId}`)
|
| 972 |
.then(response => {
|
| 973 |
if (!response.ok) throw new Error(`Ошибка ${response.status}`);
|
| 974 |
return response.text();
|
|
|
|
| 1622 |
<td>{{ "%.2f"|format(order.total_price) }} {{ currency_code }}</td>
|
| 1623 |
<td>
|
| 1624 |
<a href="{{ url_for('view_order', env_id=env_id, order_id=order.id) }}" style="color: var(--bg-medium); margin-right: 15px; padding: 10px; display: inline-block;"><i class="fas fa-eye fa-lg"></i></a>
|
| 1625 |
+
<form method="POST" action="{{ url_for('delete_order', env_id=env_id, order_id=order.id) }}" style="display:inline;" onsubmit="if(!confirm('Вы уверены, что хотите удалить заказ навсегда?')) return false;">
|
| 1626 |
<button type="submit" class="delete-btn"><i class="fas fa-trash-alt"></i></button>
|
| 1627 |
</form>
|
| 1628 |
</td>
|
|
|
|
| 2578 |
{% if settings.env_mode == '2in1' %}
|
| 2579 |
<a href="{{ url_for('pos_page', env_id=env_id) }}?emp={{ emp.id }}" class="button" style="background-color: #28a745; font-size: 0.9rem;" target="_blank"><i class="fas fa-desktop"></i> Ссылка на кассу</a>
|
| 2580 |
{% endif %}
|
| 2581 |
+
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page, q=search_q) }}" style="margin: 0;" onsubmit="if(!confirm('Удалить сотрудника?')) return false;">
|
| 2582 |
<input type="hidden" name="action" value="delete_employee">
|
| 2583 |
<input type="hidden" name="emp_id" value="{{ emp.id }}">
|
| 2584 |
<button type="submit" class="delete-button" style="margin: 0;"><i class="fas fa-trash-alt"></i></button>
|
|
|
|
| 2637 |
<input type="hidden" name="block_id" value="{{ block.id }}">
|
| 2638 |
<button type="submit" class="button btn-small" style="background: #6c757d;" {% if loop.last %}disabled{% endif %}><i class="fas fa-arrow-down"></i></button>
|
| 2639 |
</form>
|
| 2640 |
+
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page, q=search_q) }}" style="margin: 0;" onsubmit="if(!confirm('Удалить блок?')) return false;">
|
| 2641 |
<input type="hidden" name="action" value="delete_block">
|
| 2642 |
<input type="hidden" name="block_id" value="{{ block.id }}">
|
| 2643 |
<button type="submit" class="button delete-button btn-small"><i class="fas fa-trash-alt"></i></button>
|
|
|
|
| 2753 |
{% for category in categories %}
|
| 2754 |
<div class="item" style="display: flex; justify-content: space-between; align-items: center;">
|
| 2755 |
<span style="font-size: 1.05rem; font-weight: 500;">{{ category }}</span>
|
| 2756 |
+
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page, q=search_q) }}" style="margin: 0;" onsubmit="if(!confirm('Вы уверены? Товары этой категории будут помечены как \\'Без категории\\'.')) return false;">
|
| 2757 |
<input type="hidden" name="action" value="delete_category">
|
| 2758 |
<input type="hidden" name="category_name" value="{{ category }}">
|
| 2759 |
<button type="submit" class="delete-button" style="margin: 0;"><i class="fas fa-trash-alt"></i></button>
|
|
|
|
| 2892 |
</div>
|
| 2893 |
|
| 2894 |
<div class="item-actions">
|
| 2895 |
+
<button type="button" class="button" onclick="toggleEditForm('edit-form-{{ product.product_id }}', '{{ product.product_id }}')"><i class="fas fa-edit"></i> Редактировать</button>
|
| 2896 |
+
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page, q=search_q) }}" style="margin:0;" onsubmit="if(!confirm('Удалить товар?')) return false; showLoadingOverlay(); return true;">
|
| 2897 |
<input type="hidden" name="action" value="delete_product">
|
| 2898 |
<input type="hidden" name="product_id" value="{{ product.get('product_id', '') }}">
|
| 2899 |
<button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
|
| 2900 |
</form>
|
| 2901 |
</div>
|
| 2902 |
|
| 2903 |
+
<div id="edit-form-{{ product.product_id }}" class="edit-form-container">
|
| 2904 |
<h4 style="margin-top: 0; font-size: 1.1rem;"><i class="fas fa-edit"></i> Редактирование</h4>
|
| 2905 |
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page, q=search_q) }}" enctype="multipart/form-data" onsubmit="showLoadingOverlay()">
|
| 2906 |
<input type="hidden" name="action" value="edit_product">
|
|
|
|
| 2909 |
<input type="text" name="name" value="{{ product['name'] }}" required>
|
| 2910 |
|
| 2911 |
<label>Заменить фотографии (выбор новых удалит старые):</label>
|
| 2912 |
+
<input type="file" id="edit_photos_{{ product.product_id }}" name="photos" accept="image/*" multiple onchange="handleFileSelect(event, 'edit_{{ product.product_id }}')">
|
| 2913 |
|
| 2914 |
<div style="margin-top: 20px; border: 1px solid #ccc; padding: 15px; border-radius: 8px; background: #fafafa;">
|
| 2915 |
<h4 style="margin-top: 0;"><i class="fas fa-tags"></i> Отметки товаров на фото</h4>
|
| 2916 |
+
<div id="thumbs-edit_{{ product.product_id }}" class="thumbnail-row"></div>
|
| 2917 |
+
<input type="hidden" name="tags_json" id="tags_json_edit_{{ product.product_id }}" value='{{ product.get("tags",[])|tojson|safe }}'>
|
| 2918 |
+
<div id="tagging-container-edit_{{ product.product_id }}" class="tagging-container">
|
| 2919 |
+
<img id="tagging-img-edit_{{ product.product_id }}" class="tagging-img" onclick="handleTagClick(event, 'edit_{{ product.product_id }}')">
|
| 2920 |
+
<div id="tag-markers-edit_{{ product.product_id }}"></div>
|
| 2921 |
</div>
|
| 2922 |
+
<div id="tags-list-edit_{{ product.product_id }}" style="margin-top: 15px;"></div>
|
| 2923 |
</div>
|
| 2924 |
|
| 2925 |
<label>Описание:</label>
|
| 2926 |
+
<textarea id="edit_description_{{ product.product_id }}" name="description" rows="4">{{ product.get('description', '') }}</textarea>
|
| 2927 |
|
| 2928 |
<label>Категория:</label>
|
| 2929 |
<select name="category">
|
|
|
|
| 2934 |
</select>
|
| 2935 |
|
| 2936 |
<div style="margin-top: 20px; display: flex; align-items: center; gap: 8px;">
|
| 2937 |
+
<input type="checkbox" id="edit_in_stock_{{ product.product_id }}" name="in_stock" {% if product.get('in_stock', True) %}checked{% endif %}>
|
| 2938 |
+
<label for="edit_in_stock_{{ product.product_id }}" class="inline-label" style="margin: 0;">В наличии</label>
|
| 2939 |
</div>
|
| 2940 |
<div style="margin-top: 10px; display: flex; align-items: center; gap: 8px;">
|
| 2941 |
+
<input type="checkbox" id="edit_is_top_{{ product.product_id }}" name="is_top" {% if product.get('is_top', False) %}checked{% endif %}>
|
| 2942 |
+
<label for="edit_is_top_{{ product.product_id }}" class="inline-label" style="margin: 0;">Топ товар</label>
|
| 2943 |
</div>
|
| 2944 |
<br>
|
| 2945 |
<button type="submit" class="add-button" style="margin-top: 25px;"><i class="fas fa-save"></i> Сохранить</button>
|
|
|
|
| 3037 |
if(!formStates[scope]) formStates[scope] = { tags:[], fileUrls:[], currentIdx: 0, isEdit: scope.startsWith('edit_') };
|
| 3038 |
}
|
| 3039 |
|
| 3040 |
+
function toggleEditForm(formId, productId) {
|
| 3041 |
const formContainer = document.getElementById(formId);
|
| 3042 |
if (formContainer) {
|
| 3043 |
const isOpening = formContainer.style.display === 'none' || formContainer.style.display === '';
|
| 3044 |
formContainer.style.display = isOpening ? 'block' : 'none';
|
| 3045 |
if (isOpening) {
|
| 3046 |
+
const scope = `edit_${productId}`;
|
| 3047 |
+
const product = allProductsForAdmin.find(p => p.product_id === productId);
|
| 3048 |
initScope(scope);
|
| 3049 |
|
| 3050 |
let tags =[];
|
|
|
|
| 3281 |
}
|
| 3282 |
|
| 3283 |
window.openAdminPost = function(pid) {
|
| 3284 |
+
const el = document.getElementById(`edit-form-${pid}`);
|
| 3285 |
+
if (el && (el.style.display === 'none' || el.style.display === '')) {
|
| 3286 |
+
toggleEditForm(`edit-form-${pid}`, pid);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3287 |
}
|
| 3288 |
+
if (el) el.scrollIntoView({behavior: 'smooth', block: 'center'});
|
| 3289 |
};
|
| 3290 |
|
| 3291 |
async function sendAdminAi(predefinedText) {
|
|
|
|
| 3318 |
</body>
|
| 3319 |
</html>
|
| 3320 |
'''
|
|
|
|
| 3321 |
@app.route('/')
|
| 3322 |
def index():
|
| 3323 |
return render_template_string(LANDING_PAGE_TEMPLATE)
|
|
|
|
| 3349 |
if new_id not in all_data:
|
| 3350 |
break
|
| 3351 |
all_data[new_id] = {
|
| 3352 |
+
'products': [], 'categories':[], 'orders': {}, 'employees':[], 'blocks':[],
|
| 3353 |
'organization_info': {
|
| 3354 |
"about_us": "Мы — Gippo312, ваш надежный партнер в мире уникальных товаров.",
|
| 3355 |
"shipping": "Доставка осуществляется по всему Кыргызстану.",
|
|
|
|
| 3699 |
save_env_data(env_id, data)
|
| 3700 |
return jsonify({"status": "ok"})
|
| 3701 |
|
| 3702 |
+
@app.route('/<env_id>/product/<product_id>')
|
| 3703 |
+
def product_detail(env_id, product_id):
|
| 3704 |
data = get_env_data(env_id)
|
| 3705 |
all_products_raw = data.get('products',[])
|
| 3706 |
settings = data.get('settings', {})
|
|
|
|
| 3721 |
products_in_stock.append(p)
|
| 3722 |
|
| 3723 |
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
|
| 3724 |
+
|
| 3725 |
+
product = next((p for p in products_sorted if p.get('product_id') == product_id), None)
|
| 3726 |
+
if not product:
|
| 3727 |
return "Товар не найден или отсутствует в наличии.", 404
|
| 3728 |
+
|
| 3729 |
return render_template_string(
|
| 3730 |
PRODUCT_DETAIL_TEMPLATE, product=product, repo_id=REPO_ID,
|
| 3731 |
currency_code=settings.get('currency_code', 'KGS'), settings=settings, env_id=env_id
|