Update app.py
Browse files
app.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import os
|
| 2 |
import io
|
| 3 |
import base64
|
|
@@ -546,14 +547,28 @@ CATALOG_TEMPLATE = '''
|
|
| 546 |
.cart-fab { position: fixed; bottom: 24px; right: 24px; background: var(--accent); color: var(--text-dark); width: 64px; height: 64px; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 1.6rem; cursor: pointer; box-shadow: 0 6px 20px rgba(0,0,0,0.3); z-index: 999; display: none; }
|
| 547 |
.cart-badge { position: absolute; top: -2px; right: -2px; background: var(--danger); color: white; border-radius: 50%; width: 26px; height: 26px; font-size: 0.85rem; display: flex; justify-content: center; align-items: center; font-weight: bold; border: 2px solid var(--bg-dark); }
|
| 548 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
.cart-item { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid rgba(255,255,255,0.1); gap: 15px;}
|
| 550 |
-
.cart-item img { width:
|
| 551 |
-
.cart-item
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
.cart-controls input::-webkit-outer-spin-button, .cart-controls input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
|
|
|
|
|
|
|
|
|
| 557 |
.add-to-cart-btn { background: var(--accent); color: var(--text-dark); border: none; padding: 12px; border-radius: 26px; font-weight: bold; cursor: pointer; width: 100%; font-size: 1.1rem; box-shadow: 0 4px 15px rgba(0,0,0,0.2); }
|
| 558 |
.add-to-cart-btn:hover { background: var(--accent-hover); }
|
| 559 |
|
|
@@ -657,12 +672,19 @@ CATALOG_TEMPLATE = '''
|
|
| 657 |
<div id="cartModal" class="modal">
|
| 658 |
<div class="modal-content" style="padding: 24px; display: flex; flex-direction: column; min-height: 80vh; max-height: 95vh;">
|
| 659 |
<button class="close-btn" onclick="closeModal('cartModal')"><i class="fas fa-times"></i></button>
|
| 660 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
<div id="cartItems" style="flex-grow: 1; overflow-y: auto; padding-right: 5px;"></div>
|
|
|
|
| 662 |
<div id="cartTotal" style="font-size: 1.4rem; font-weight: bold; margin-top: 20px; text-align: right; color: var(--accent);"></div>
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
<input type="
|
|
|
|
| 666 |
<button onclick="submitOrder()" class="save-contact-btn" style="width: 100%; margin-bottom: 0; box-shadow: none;">Оформить заказ</button>
|
| 667 |
</div>
|
| 668 |
</div>
|
|
@@ -764,6 +786,29 @@ CATALOG_TEMPLATE = '''
|
|
| 764 |
renderCartModal();
|
| 765 |
renderModalCartControls(productId);
|
| 766 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 767 |
|
| 768 |
function setCategory(cat, btn) {
|
| 769 |
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
|
|
@@ -805,7 +850,7 @@ CATALOG_TEMPLATE = '''
|
|
| 805 |
const paginated = filtered.slice(start, start + itemsPerPage);
|
| 806 |
|
| 807 |
if (filtered.length === 0) {
|
| 808 |
-
container.innerHTML = '<div class="no-results-message">В этой
|
| 809 |
return;
|
| 810 |
}
|
| 811 |
|
|
@@ -941,31 +986,40 @@ CATALOG_TEMPLATE = '''
|
|
| 941 |
function renderCartModal() {
|
| 942 |
const container = document.getElementById('cartItems');
|
| 943 |
const totalContainer = document.getElementById('cartTotal');
|
|
|
|
|
|
|
| 944 |
let html = '';
|
| 945 |
let total = 0;
|
|
|
|
| 946 |
for(let id in cart) {
|
| 947 |
let item = cart[id];
|
| 948 |
total += item.qty * item.price;
|
| 949 |
-
let photoUrl = item.photo ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}` : `https://via.placeholder.com/
|
| 950 |
html += `
|
| 951 |
<div class="cart-item">
|
| 952 |
-
<img src="${photoUrl}" alt="Фото">
|
| 953 |
<div class="cart-item-info">
|
| 954 |
<div class="cart-item-price">${item.price.toFixed(0)} ${currencyCode}</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 955 |
</div>
|
| 956 |
-
<
|
| 957 |
-
<button onclick="updateQty('${id}', -1)">-</button>
|
| 958 |
-
<input type="number" value="${item.qty}" onchange="setQty('${id}', this)">
|
| 959 |
-
<button onclick="updateQty('${id}', 1)">+</button>
|
| 960 |
-
</div>
|
| 961 |
</div>
|
| 962 |
`;
|
| 963 |
}
|
|
|
|
| 964 |
if(html === '') {
|
| 965 |
-
html = '<p style="text-align:center; opacity:0.7;">
|
| 966 |
totalContainer.innerHTML = '';
|
|
|
|
|
|
|
| 967 |
} else {
|
| 968 |
totalContainer.innerHTML = `Итого: ${total.toFixed(0)} ${currencyCode}`;
|
|
|
|
|
|
|
| 969 |
}
|
| 970 |
container.innerHTML = html;
|
| 971 |
}
|
|
@@ -1027,99 +1081,133 @@ ADMIN_TEMPLATE = '''
|
|
| 1027 |
<meta charset="UTF-8">
|
| 1028 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 1029 |
<title>Настройки Визитки - {{ settings.organization_name }}</title>
|
| 1030 |
-
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@
|
| 1031 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 1032 |
<style>
|
| 1033 |
:root { --bg-light: #f4f6f9; --bg-medium: #135D66; --accent: #48D1CC; --accent-hover: #77E4D8; --text-dark: #333; --text-on-accent: #003C43; --danger: #E57373; --danger-hover: #EF5350; }
|
| 1034 |
* { box-sizing: border-box; }
|
| 1035 |
-
body { font-family: 'Montserrat', sans-serif; background-color: var(--bg-light); color: var(--text-dark); padding: 20px; line-height: 1.6; margin: 0; }
|
| 1036 |
-
|
| 1037 |
-
.
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1041 |
h2 { font-size: 1.5rem; margin-top: 20px; display: flex; align-items: center; gap: 10px; }
|
| 1042 |
-
h3 { font-size: 1.
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
input
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
input
|
| 1052 |
-
|
| 1053 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1054 |
button:hover, .button:hover { background-color: var(--accent-hover); }
|
| 1055 |
button:active, .button:active { transform: scale(0.98); }
|
|
|
|
| 1056 |
.delete-button { background-color: var(--danger); color: white; }
|
| 1057 |
.delete-button:hover { background-color: var(--danger-hover); }
|
| 1058 |
.add-button { background-color: var(--bg-medium); color: white; }
|
| 1059 |
.add-button:hover { background-color: #003C43; }
|
|
|
|
| 1060 |
.item-list { display: grid; gap: 20px; }
|
| 1061 |
-
.item { background: #fff; padding:
|
| 1062 |
-
.item
|
| 1063 |
-
.item p { margin: 6px 0; font-size: 0.95rem; color: #555; }
|
| 1064 |
.item strong { color: var(--text-dark); }
|
| 1065 |
-
.item-actions { margin-top:
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1070 |
details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 24px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; color: var(--bg-medium); }
|
| 1071 |
details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
|
| 1072 |
-
details[open] > summary { border-bottom:
|
| 1073 |
-
details .form-content { padding:
|
| 1074 |
-
|
| 1075 |
-
.photo-preview
|
| 1076 |
-
.
|
| 1077 |
-
|
| 1078 |
-
.message
|
| 1079 |
-
.
|
| 1080 |
-
.
|
| 1081 |
-
|
| 1082 |
-
.
|
| 1083 |
-
|
| 1084 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1085 |
#loadingOverlay {
|
| 1086 |
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
| 1087 |
background: rgba(0,0,0,0.85); backdrop-filter: blur(5px); z-index: 10000; flex-direction: column;
|
| 1088 |
justify-content: center; align-items: center; color: white; text-align: center;
|
| 1089 |
}
|
| 1090 |
.spinner {
|
| 1091 |
-
width:
|
| 1092 |
border-top: 6px solid var(--accent); border-radius: 50%;
|
| 1093 |
animation: spin 1s linear infinite; margin-bottom: 25px;
|
| 1094 |
}
|
| 1095 |
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
| 1096 |
-
@media (max-width: 600px) {
|
| 1097 |
-
.header { flex-direction: column; align-items: flex-start; }
|
| 1098 |
-
}
|
| 1099 |
</style>
|
| 1100 |
</head>
|
| 1101 |
<body>
|
| 1102 |
<div id="loadingOverlay">
|
| 1103 |
<div class="spinner"></div>
|
| 1104 |
-
<h2 style="color:white; margin:0;">
|
| 1105 |
</div>
|
| 1106 |
<div class="container">
|
| 1107 |
<div class="header">
|
| 1108 |
-
<div class="logo-title-container"
|
| 1109 |
<img src="{{ chat_avatar_url }}" alt="Logo">
|
| 1110 |
-
<h1 style="margin:0;"><i class="fas fa-id-badge"></i> Настройки</h1>
|
| 1111 |
</div>
|
| 1112 |
-
<div
|
| 1113 |
-
<a href="{{ url_for('catalog', env_id=env_id) }}" class="button" style="background-color: var(--bg-medium); color: white;"><i class="fas fa-external-link-alt"></i> Открыть</a>
|
| 1114 |
<a href="{{ url_for('admin_orders', env_id=env_id) }}" class="button" style="background-color: {% if new_orders_count > 0 %}#E57373{% else %}var(--bg-medium){% endif %}; color: white; position: relative;">
|
| 1115 |
<i class="fas fa-history"></i> Заказы
|
| 1116 |
{% if new_orders_count > 0 %}
|
| 1117 |
-
<span style="background-color: white; color: #E57373; border-radius: 50%; padding:
|
| 1118 |
{% endif %}
|
| 1119 |
</a>
|
| 1120 |
-
<button onclick="downloadQR()" class="button" style="background-color: #8A2BE2; color: white;"><i class="fas fa-qrcode"></i> QR</button>
|
| 1121 |
{% if settings.admin_password_enabled %}
|
| 1122 |
-
<a href="{{ url_for('admin_logout', env_id=env_id) }}" class="button" style="background-color: #6c757d; color: white;"><i class="fas fa-sign-out-alt"></i> Вы
|
| 1123 |
{% endif %}
|
| 1124 |
</div>
|
| 1125 |
</div>
|
|
@@ -1150,80 +1238,80 @@ ADMIN_TEMPLATE = '''
|
|
| 1150 |
</div>
|
| 1151 |
</div>
|
| 1152 |
|
| 1153 |
-
<label>
|
| 1154 |
<input type="text" name="vcard_job" value="{{ settings.vcard_job }}">
|
| 1155 |
|
| 1156 |
<label>Название организации:</label>
|
| 1157 |
<input type="text" name="organization_name" value="{{ settings.organization_name }}">
|
| 1158 |
|
| 1159 |
-
<label>Коротко о себе
|
| 1160 |
<textarea name="about_text" rows="3">{{ settings.about_text }}</textarea>
|
| 1161 |
|
| 1162 |
-
<label>
|
| 1163 |
<input type="file" name="chat_avatar" accept="image/*">
|
| 1164 |
{% if settings.chat_avatar %}
|
| 1165 |
-
<p style="font-size:
|
| 1166 |
{% endif %}
|
| 1167 |
|
| 1168 |
-
<label>Цветовая схема:</label>
|
| 1169 |
<select name="color_scheme">
|
| 1170 |
{% for key, name in color_schemes.items() %}
|
| 1171 |
<option value="{{ key }}" {% if settings.color_scheme == key %}selected{% endif %}>{{ name }}</option>
|
| 1172 |
{% endfor %}
|
| 1173 |
</select>
|
| 1174 |
|
| 1175 |
-
<label>Валюта
|
| 1176 |
<select name="currency_code">
|
| 1177 |
{% for code, name in currencies.items() %}
|
| 1178 |
<option value="{{ code }}" {% if settings.currency_code == code %}selected{% endif %}>{{ name }} ({{ code }})</option>
|
| 1179 |
{% endfor %}
|
| 1180 |
</select>
|
| 1181 |
|
| 1182 |
-
<h4 style="margin-top:
|
| 1183 |
-
<label style="display: flex; align-items: center; gap:
|
| 1184 |
<input type="checkbox" name="enable_cart" {% if settings.enable_cart %}checked{% endif %}>
|
| 1185 |
-
Включить возможность заказа
|
| 1186 |
</label>
|
| 1187 |
-
<label>
|
| 1188 |
<select name="order_messenger">
|
| 1189 |
-
<option value="whatsapp" {% if settings.order_messenger == 'whatsapp' %}selected{% endif %}>WhatsApp</option>
|
| 1190 |
-
<option value="telegram" {% if settings.order_messenger == 'telegram' %}selected{% endif %}>Telegram</option>
|
| 1191 |
<option value="none" {% if settings.order_messenger == 'none' %}selected{% endif %}>Не принимать заказы</option>
|
| 1192 |
</select>
|
| 1193 |
-
<label>
|
| 1194 |
-
<input type="text" name="order_contact" value="{{ settings.order_contact }}" placeholder="Например: 996555123456 или
|
| 1195 |
|
| 1196 |
-
<div style="background: #f1f3f5; padding:
|
| 1197 |
-
<h4 style="margin-top: 0; color: var(--bg-medium); font-size: 1.
|
| 1198 |
-
<label style="display: flex; align-items: center; gap:
|
| 1199 |
<label style="margin-top: 15px;">Пароль:</label>
|
| 1200 |
<input type="text" name="admin_password" value="{{ settings.admin_password }}" placeholder="Текущий пароль">
|
| 1201 |
</div>
|
| 1202 |
|
| 1203 |
-
<button type="submit" class="add-button" style="margin-top:
|
| 1204 |
</form>
|
| 1205 |
</div>
|
| 1206 |
</details>
|
| 1207 |
</div>
|
| 1208 |
|
| 1209 |
<div class="section">
|
| 1210 |
-
<h2><i class="fas fa-link"></i>
|
| 1211 |
<details>
|
| 1212 |
-
<summary><i class="fas fa-plus-circle"></i>
|
| 1213 |
<div class="form-content">
|
| 1214 |
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" enctype="multipart/form-data" onsubmit="showLoadingOverlay()">
|
| 1215 |
<input type="hidden" name="action" value="add_block">
|
| 1216 |
-
<label>Тип
|
| 1217 |
<select name="block_type" id="add_block_type" onchange="toggleBlockFields('add_')">
|
| 1218 |
-
<option value="link">
|
| 1219 |
-
<option value="text">Текст
|
| 1220 |
-
<option value="pdf">PDF
|
| 1221 |
</select>
|
| 1222 |
-
<label>
|
| 1223 |
<input type="text" name="block_title" required>
|
| 1224 |
|
| 1225 |
<div id="add_block_icon_div">
|
| 1226 |
-
<label>Иконка:</label>
|
| 1227 |
<select name="block_icon">
|
| 1228 |
<option value="">-- Без иконки --</option>
|
| 1229 |
{% for icon_class, icon_name in icons.items() %}
|
|
@@ -1233,54 +1321,54 @@ ADMIN_TEMPLATE = '''
|
|
| 1233 |
</div>
|
| 1234 |
|
| 1235 |
<div id="add_block_url_div">
|
| 1236 |
-
<label>
|
| 1237 |
-
<input type="text" name="block_url" placeholder="https://... или +996...
|
| 1238 |
</div>
|
| 1239 |
<div id="add_block_pdf_div" style="display: none;">
|
| 1240 |
-
<label>
|
| 1241 |
<input type="file" name="block_pdf" accept="application/pdf">
|
| 1242 |
</div>
|
| 1243 |
<div id="add_block_content_div" style="display: none;">
|
| 1244 |
-
<label>Текст
|
| 1245 |
<textarea name="block_content" rows="3"></textarea>
|
| 1246 |
</div>
|
| 1247 |
-
<button type="submit" class="add-button"><i class="fas fa-plus"></i> Добавить</button>
|
| 1248 |
</form>
|
| 1249 |
</div>
|
| 1250 |
</details>
|
| 1251 |
-
<div style="margin-top:
|
| 1252 |
{% if blocks %}
|
| 1253 |
{% for block in blocks %}
|
| 1254 |
<div class="block-item">
|
| 1255 |
<div style="flex-grow: 1;">
|
| 1256 |
-
<strong style="font-size: 1.
|
| 1257 |
{% if block.icon %}
|
| 1258 |
{% set prefix = 'fab' if block.icon in ['fa-whatsapp', 'fa-telegram', 'fa-instagram', 'fa-youtube', 'fa-tiktok'] else 'fas' %}
|
| 1259 |
<i class="{{ prefix }} {{ block.icon }}" style="color:var(--accent);"></i>
|
| 1260 |
{% endif %}
|
| 1261 |
{{ block.title }}
|
| 1262 |
</strong>
|
| 1263 |
-
<span style="color: #
|
| 1264 |
({% if block.type == 'link' %}Ссылка{% elif block.type == 'pdf' %}PDF{% else %}Текст{% endif %})
|
| 1265 |
</span>
|
| 1266 |
-
{% if block.type == 'link' %}<br><small style="font-size:
|
| 1267 |
</div>
|
| 1268 |
<div class="block-controls">
|
| 1269 |
-
<button type="button" class="button btn-small" style="background: #1E90FF;" onclick="toggleEditForm('edit-block-{{ block.id }}'); toggleBlockFields('edit_{{ block.id }}_');"><i class="fas fa-edit"></i></button>
|
| 1270 |
-
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" style="margin: 0;">
|
| 1271 |
<input type="hidden" name="action" value="move_block_up">
|
| 1272 |
<input type="hidden" name="block_id" value="{{ block.id }}">
|
| 1273 |
-
<button type="submit" class="button btn-small" style="background: #6c757d;" {% if loop.first %}disabled{% endif %}><i class="fas fa-arrow-up"></i></button>
|
| 1274 |
</form>
|
| 1275 |
-
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" style="margin: 0;">
|
| 1276 |
<input type="hidden" name="action" value="move_block_down">
|
| 1277 |
<input type="hidden" name="block_id" value="{{ block.id }}">
|
| 1278 |
-
<button type="submit" class="button btn-small" style="background: #6c757d;" {% if loop.last %}disabled{% endif %}><i class="fas fa-arrow-down"></i></button>
|
| 1279 |
</form>
|
| 1280 |
-
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" style="margin: 0;" onsubmit="if(!confirm('Удалить
|
| 1281 |
<input type="hidden" name="action" value="delete_block">
|
| 1282 |
<input type="hidden" name="block_id" value="{{ block.id }}">
|
| 1283 |
-
<button type="submit" class="button delete-button btn-small"><i class="fas fa-trash-alt"></i></button>
|
| 1284 |
</form>
|
| 1285 |
</div>
|
| 1286 |
</div>
|
|
@@ -1290,13 +1378,13 @@ ADMIN_TEMPLATE = '''
|
|
| 1290 |
<input type="hidden" name="block_id" value="{{ block.id }}">
|
| 1291 |
<input type="hidden" name="old_block_url" value="{{ block.url }}">
|
| 1292 |
|
| 1293 |
-
<label>Тип
|
| 1294 |
<select name="block_type" id="edit_{{ block.id }}_block_type" onchange="toggleBlockFields('edit_{{ block.id }}_')">
|
| 1295 |
-
<option value="link" {% if block.type == 'link' %}selected{% endif %}>
|
| 1296 |
-
<option value="text" {% if block.type == 'text' %}selected{% endif %}>Текст
|
| 1297 |
-
<option value="pdf" {% if block.type == 'pdf' %}selected{% endif %}>PDF
|
| 1298 |
</select>
|
| 1299 |
-
<label>
|
| 1300 |
<input type="text" name="block_title" value="{{ block.title }}" required>
|
| 1301 |
|
| 1302 |
<div id="edit_{{ block.id }}_block_icon_div">
|
|
@@ -1310,24 +1398,24 @@ ADMIN_TEMPLATE = '''
|
|
| 1310 |
</div>
|
| 1311 |
|
| 1312 |
<div id="edit_{{ block.id }}_block_url_div">
|
| 1313 |
-
<label>
|
| 1314 |
<input type="text" name="block_url" value="{{ block.url if block.type == 'link' else '' }}">
|
| 1315 |
</div>
|
| 1316 |
<div id="edit_{{ block.id }}_block_pdf_div" style="display: none;">
|
| 1317 |
<label>Текущий PDF: {% if block.type == 'pdf' and block.url %}{{ block.url }}{% else %}Нет{% endif %}</label>
|
| 1318 |
-
<label>Загрузить новый PDF (
|
| 1319 |
<input type="file" name="block_pdf" accept="application/pdf">
|
| 1320 |
</div>
|
| 1321 |
<div id="edit_{{ block.id }}_block_content_div" style="display: none;">
|
| 1322 |
-
<label>Текст
|
| 1323 |
<textarea name="block_content" rows="3">{{ block.content }}</textarea>
|
| 1324 |
</div>
|
| 1325 |
-
<button type="submit" class="add-button"><i class="fas fa-save"></i> Сохранить</button>
|
| 1326 |
</form>
|
| 1327 |
</div>
|
| 1328 |
{% endfor %}
|
| 1329 |
{% else %}
|
| 1330 |
-
<p style="font-size: 1.
|
| 1331 |
{% endif %}
|
| 1332 |
</div>
|
| 1333 |
<script>
|
|
@@ -1352,117 +1440,122 @@ ADMIN_TEMPLATE = '''
|
|
| 1352 |
</div>
|
| 1353 |
|
| 1354 |
<div class="section">
|
| 1355 |
-
<h2><i class="fas fa-
|
| 1356 |
<details>
|
| 1357 |
-
<summary><i class="fas fa-plus-circle"></i>
|
| 1358 |
<div class="form-content">
|
| 1359 |
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}">
|
| 1360 |
<input type="hidden" name="action" value="add_category">
|
| 1361 |
-
<label>Название новой
|
| 1362 |
<input type="text" name="category_name" required>
|
| 1363 |
-
<button type="submit" class="add-button"><i class="fas fa-plus"></i>
|
| 1364 |
</form>
|
| 1365 |
</div>
|
| 1366 |
</details>
|
| 1367 |
{% if categories %}
|
| 1368 |
-
<div class="item-list" style="margin-top:
|
| 1369 |
{% for category in categories %}
|
| 1370 |
-
<div class="item" style="display: flex; justify-content: space-between; align-items: center;">
|
| 1371 |
-
<span style="font-size: 1.
|
| 1372 |
-
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" style="margin: 0;" onsubmit="if(!confirm('
|
| 1373 |
<input type="hidden" name="action" value="delete_category">
|
| 1374 |
<input type="hidden" name="category_name" value="{{ category }}">
|
| 1375 |
-
<button type="submit" class="delete-button" style="margin: 0;"><i class="fas fa-trash-alt"></i></button>
|
| 1376 |
</form>
|
| 1377 |
</div>
|
| 1378 |
{% endfor %}
|
| 1379 |
</div>
|
| 1380 |
{% else %}
|
| 1381 |
-
<p style="font-size: 1.
|
| 1382 |
{% endif %}
|
| 1383 |
</div>
|
| 1384 |
|
| 1385 |
<div class="section">
|
| 1386 |
-
<h2><i class="fas fa-
|
| 1387 |
<details open>
|
| 1388 |
-
<summary><i class="fas fa-
|
| 1389 |
<div class="form-content">
|
| 1390 |
-
<label>
|
| 1391 |
<select id="bulk_category">
|
| 1392 |
-
<option value="Без категории">Без
|
| 1393 |
{% for category in categories %}
|
| 1394 |
<option value="{{ category }}">{{ category }}</option>
|
| 1395 |
{% endfor %}
|
| 1396 |
</select>
|
| 1397 |
|
| 1398 |
-
<label style="margin-top:
|
| 1399 |
<input type="file" id="bulk_photos" accept="image/*" multiple onchange="handleBulkSelect(event)">
|
| 1400 |
|
| 1401 |
-
<div id="bulk_preview" style="display: flex; flex-wrap: wrap; gap: 15px; margin-top:
|
| 1402 |
|
| 1403 |
-
<div id="progress_container" style="display:none; margin-top:
|
| 1404 |
-
<div style="width:100%; background:#eee; border-radius:
|
| 1405 |
-
<div id="progress_bar" style="width:0%; height:
|
| 1406 |
</div>
|
| 1407 |
-
<p id="progress_text" style="text-align:center; font-weight:bold; margin-top:
|
| 1408 |
</div>
|
| 1409 |
|
| 1410 |
-
<button type="button" class="add-button" style="margin-top:
|
| 1411 |
</div>
|
| 1412 |
</details>
|
| 1413 |
|
| 1414 |
-
<h3 style="margin-top:
|
| 1415 |
{% if paginated_products %}
|
| 1416 |
<div class="item-list" id="admin-products-list">
|
| 1417 |
{% for product in paginated_products %}
|
| 1418 |
<div class="item">
|
| 1419 |
-
<div style="display: flex; gap: 20px; align-items: flex-start;">
|
| 1420 |
-
<
|
| 1421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1422 |
{% if product.get('photos') %}
|
| 1423 |
{% for p in product['photos'] %}
|
| 1424 |
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ p }}" alt="Фото">
|
| 1425 |
{% endfor %}
|
| 1426 |
{% else %}
|
| 1427 |
-
<img src="https://via.placeholder.com/
|
| 1428 |
{% endif %}
|
| 1429 |
</div>
|
| 1430 |
-
|
| 1431 |
-
|
| 1432 |
-
<p style="font-size: 1.
|
|
|
|
| 1433 |
</div>
|
| 1434 |
</div>
|
| 1435 |
|
| 1436 |
<div class="item-actions">
|
| 1437 |
-
<button type="button" class="button" onclick="toggleEditForm('edit-form-{{ product.product_id }}')"><i class="fas fa-edit"></i>
|
| 1438 |
-
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" style="margin:0;" onsubmit="if(!confirm('
|
| 1439 |
<input type="hidden" name="action" value="delete_product">
|
| 1440 |
<input type="hidden" name="product_id" value="{{ product.get('product_id', '') }}">
|
| 1441 |
-
<button type="submit" class="delete-button button"><i class="fas fa-trash-alt"></i></button>
|
| 1442 |
</form>
|
| 1443 |
</div>
|
| 1444 |
|
| 1445 |
<div id="edit-form-{{ product.product_id }}" class="edit-form-container">
|
| 1446 |
-
<h4 style="margin-top: 0; font-size: 1.
|
| 1447 |
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" enctype="multipart/form-data" onsubmit="showLoadingOverlay()">
|
| 1448 |
<input type="hidden" name="action" value="edit_product">
|
| 1449 |
<input type="hidden" name="product_id" value="{{ product.get('product_id', '') }}">
|
| 1450 |
|
| 1451 |
-
<label>
|
| 1452 |
<input type="number" name="price" step="0.01" value="{{ product.get('price', 0) }}">
|
| 1453 |
|
| 1454 |
-
<label>
|
| 1455 |
<select name="category">
|
| 1456 |
-
<option value="Без категории">Без
|
| 1457 |
{% for category in categories %}
|
| 1458 |
<option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
|
| 1459 |
{% endfor %}
|
| 1460 |
</select>
|
| 1461 |
|
| 1462 |
-
<label>За
|
| 1463 |
<input type="file" name="photos" accept="image/*" multiple>
|
| 1464 |
|
| 1465 |
-
<button type="submit" class="add-button" style="margin-top: 25px;"><i class="fas fa-save"></i> Сохранить</button>
|
| 1466 |
</form>
|
| 1467 |
</div>
|
| 1468 |
</div>
|
|
@@ -1472,25 +1565,25 @@ ADMIN_TEMPLATE = '''
|
|
| 1472 |
{% if total_pages > 1 %}
|
| 1473 |
<div class="pagination">
|
| 1474 |
{% if page > 1 %}
|
| 1475 |
-
<a href="{{ url_for('admin', env_id=env_id, p=page-1) }}" class="button"
|
| 1476 |
{% endif %}
|
| 1477 |
|
| 1478 |
{% for p_num in range(1, total_pages + 1) %}
|
| 1479 |
{% if p_num == 1 or p_num == total_pages or (p_num >= page - 2 and p_num <= page + 2) %}
|
| 1480 |
-
<a href="{{ url_for('admin', env_id=env_id, p=p_num) }}" class="button {% if p_num == page %}active{% endif %}" style="{% if p_num == page %}background-color: var(--accent); color: var(--text-dark);{% else %}background-color: var(--bg-medium); color: white;{% endif %}">{{ p_num }}</a>
|
| 1481 |
{% elif p_num == page - 3 or p_num == page + 3 %}
|
| 1482 |
-
<span style="padding:
|
| 1483 |
{% endif %}
|
| 1484 |
{% endfor %}
|
| 1485 |
|
| 1486 |
{% if page < total_pages %}
|
| 1487 |
-
<a href="{{ url_for('admin', env_id=env_id, p=page+1) }}" class="button"
|
| 1488 |
{% endif %}
|
| 1489 |
</div>
|
| 1490 |
{% endif %}
|
| 1491 |
|
| 1492 |
{% else %}
|
| 1493 |
-
<p style="font-size: 1.
|
| 1494 |
{% endif %}
|
| 1495 |
</div>
|
| 1496 |
</div>
|
|
@@ -1500,12 +1593,12 @@ ADMIN_TEMPLATE = '''
|
|
| 1500 |
<div id="bulk-delete-inputs"></div>
|
| 1501 |
</form>
|
| 1502 |
|
| 1503 |
-
<button id="bulk-delete-fab" class="button delete-button" style="display:none; position:fixed; bottom:24px; left:24px; z-index:999; border-radius:
|
| 1504 |
-
<i class="fas fa-trash-alt"></i> Удалить (<span id="bulk-delete-count">0</span>)
|
| 1505 |
</button>
|
| 1506 |
|
| 1507 |
-
<button id="carousel-fab" class="button" style="display:none; position:fixed; bottom:24px; right:24px; z-index:999; border-radius:
|
| 1508 |
-
<i class="fas fa-object-group"></i>
|
| 1509 |
</button>
|
| 1510 |
|
| 1511 |
<script>
|
|
@@ -1530,16 +1623,16 @@ ADMIN_TEMPLATE = '''
|
|
| 1530 |
bulkFiles.forEach(item => {
|
| 1531 |
const div = document.createElement('div');
|
| 1532 |
div.id = 'preview_div_' + item.id;
|
| 1533 |
-
div.style.cssText = 'border:
|
| 1534 |
|
| 1535 |
-
let badge = item.files.length > 1 ? `<div style="position:absolute;top:-
|
| 1536 |
|
| 1537 |
div.innerHTML = `
|
| 1538 |
-
<input type="checkbox" class="carousel-group-checkbox" value="${item.id}" style="position:absolute; top:
|
| 1539 |
${badge}
|
| 1540 |
-
<img src="${item.previewUrl}" style="width:100%; height:
|
| 1541 |
-
<input type="number" id="price_${item.id}" placeholder="Цена" value="0" style="width:100%; padding:
|
| 1542 |
-
<button type="button" class="button delete-button" style="width:100%; padding:
|
| 1543 |
`;
|
| 1544 |
container.appendChild(div);
|
| 1545 |
});
|
|
@@ -1612,7 +1705,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1612 |
}
|
| 1613 |
|
| 1614 |
async function uploadBulk() {
|
| 1615 |
-
if(bulkFiles.length === 0) return alert('
|
| 1616 |
const category = document.getElementById('bulk_category').value;
|
| 1617 |
const formData = new FormData();
|
| 1618 |
formData.append('action', 'bulk_add');
|
|
@@ -1622,7 +1715,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1622 |
document.getElementById('progress_container').style.display = 'block';
|
| 1623 |
const btn = document.querySelector('button[onclick="uploadBulk()"]');
|
| 1624 |
btn.disabled = true;
|
| 1625 |
-
btn.innerText = "Подготовка файлов...";
|
| 1626 |
|
| 1627 |
for(let i=0; i<bulkFiles.length; i++) {
|
| 1628 |
let item = bulkFiles[i];
|
|
@@ -1634,7 +1727,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1634 |
}
|
| 1635 |
}
|
| 1636 |
|
| 1637 |
-
btn.innerText = "
|
| 1638 |
|
| 1639 |
const xhr = new XMLHttpRequest();
|
| 1640 |
xhr.open('POST', '{{ url_for("admin", env_id=env_id) }}', true);
|
|
@@ -1649,17 +1742,17 @@ ADMIN_TEMPLATE = '''
|
|
| 1649 |
if (xhr.status === 200) {
|
| 1650 |
window.location.reload();
|
| 1651 |
} else {
|
| 1652 |
-
alert('
|
| 1653 |
document.getElementById('progress_container').style.display = 'none';
|
| 1654 |
btn.disabled = false;
|
| 1655 |
-
btn.innerHTML = '<i class="fas fa-upload"></i>
|
| 1656 |
}
|
| 1657 |
};
|
| 1658 |
xhr.onerror = function() {
|
| 1659 |
-
alert('Сетевая ошибка');
|
| 1660 |
document.getElementById('progress_container').style.display = 'none';
|
| 1661 |
btn.disabled = false;
|
| 1662 |
-
btn.innerHTML = '<i class="fas fa-upload"></i>
|
| 1663 |
};
|
| 1664 |
xhr.send(formData);
|
| 1665 |
}
|
|
@@ -1676,7 +1769,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1676 |
}
|
| 1677 |
|
| 1678 |
function submitBulkDelete() {
|
| 1679 |
-
if(confirm('
|
| 1680 |
const checked = document.querySelectorAll('.bulk-delete-checkbox:checked');
|
| 1681 |
const container = document.getElementById('bulk-delete-inputs');
|
| 1682 |
container.innerHTML = '';
|
|
@@ -1717,7 +1810,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1717 |
link.click();
|
| 1718 |
document.body.removeChild(link);
|
| 1719 |
})
|
| 1720 |
-
.catch(err => alert('
|
| 1721 |
}
|
| 1722 |
</script>
|
| 1723 |
</body>
|
|
@@ -1827,38 +1920,38 @@ ADMIN_ORDERS_TEMPLATE = '''
|
|
| 1827 |
<style>
|
| 1828 |
:root { --bg-light: #f4f6f9; --bg-medium: #135D66; --accent: #48D1CC; --accent-hover: #77E4D8; --text-dark: #333; --text-on-accent: #003C43; --danger: #E57373; --danger-hover: #EF5350; }
|
| 1829 |
* { box-sizing: border-box; }
|
| 1830 |
-
body { font-family: 'Montserrat', sans-serif; background-color: var(--bg-light); color: var(--text-dark); padding: 20px; line-height: 1.6; margin: 0; }
|
| 1831 |
.container { max-width: 900px; margin: 0 auto; background-color: #fff; padding: 30px; border-radius: 24px; box-shadow: 0 10px 40px rgba(0,0,0,0.04); }
|
| 1832 |
-
.header { padding-bottom: 20px; margin-bottom: 25px; border-bottom:
|
| 1833 |
-
h1, h2 { font-weight:
|
| 1834 |
h1 { font-size: 1.8rem; margin: 0; }
|
| 1835 |
-
.button { padding:
|
| 1836 |
.button:hover { background-color: var(--accent-hover); }
|
| 1837 |
.button:active { transform: scale(0.98); }
|
| 1838 |
-
.back-button { background-color: #6c757d; color: white; }
|
|
|
|
| 1839 |
.back-button:hover { background-color: #5a6268; }
|
| 1840 |
-
.delete-button { background-color: var(--danger); color: white; padding:
|
| 1841 |
.delete-button:hover { background-color: var(--danger-hover); }
|
| 1842 |
-
.order-item { background: #fff; padding: 25px; border:
|
| 1843 |
-
.order-
|
| 1844 |
-
.order-
|
| 1845 |
-
.order-
|
| 1846 |
-
.
|
| 1847 |
-
.customer-info p { margin: 5px 0; font-size: 1rem; }
|
| 1848 |
.customer-info strong { color: var(--bg-medium); }
|
| 1849 |
-
.order-details { margin-top:
|
| 1850 |
-
.product-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px dashed #e0e0e0; font-size: 1.
|
| 1851 |
.product-row:last-child { border-bottom: none; }
|
| 1852 |
-
.order-total { text-align: right; font-weight: 700; color: var(--bg-medium); font-size: 1.
|
| 1853 |
.empty-state { text-align: center; padding: 60px 20px; color: #888; }
|
| 1854 |
-
.empty-state i { font-size:
|
| 1855 |
</style>
|
| 1856 |
</head>
|
| 1857 |
<body>
|
| 1858 |
<div class="container">
|
| 1859 |
<div class="header">
|
| 1860 |
<h1><i class="fas fa-history"></i> История заказов</h1>
|
| 1861 |
-
<a href="{{ url_for('admin', env_id=env_id) }}" class="button back-button"><i class="fas fa-arrow-left"></i>
|
| 1862 |
</div>
|
| 1863 |
{% if orders %}
|
| 1864 |
{% for order in orders %}
|
|
@@ -1868,8 +1961,8 @@ ADMIN_ORDERS_TEMPLATE = '''
|
|
| 1868 |
<span class="order-id">Заказ #{{ order['id'] }}</span>
|
| 1869 |
<span class="order-date">{{ order['date'] }}</span>
|
| 1870 |
</div>
|
| 1871 |
-
<div style="display: flex; gap: 10px; align-items: center;">
|
| 1872 |
-
<a href="{{ url_for('order_invoice', env_id=env_id, order_id=order['id']) }}" target="_blank" class="button" style="padding:
|
| 1873 |
<form method="POST" action="{{ url_for('delete_order', env_id=env_id, order_id=order['id']) }}" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить этот заказ из истории?');">
|
| 1874 |
<button type="submit" class="button delete-button"><i class="fas fa-trash-alt"></i></button>
|
| 1875 |
</form>
|
|
@@ -1877,17 +1970,17 @@ ADMIN_ORDERS_TEMPLATE = '''
|
|
| 1877 |
</div>
|
| 1878 |
<div class="customer-info">
|
| 1879 |
<p><strong>Покупатель:</strong> {{ order['customer_name'] }}</p>
|
| 1880 |
-
<p><strong>Телефон:</strong> <a href="tel:{{ order['customer_phone'] }}" style="color:var(--accent); font-weight:
|
| 1881 |
</div>
|
| 1882 |
<div class="order-details">
|
| 1883 |
{% for pid, item in order['items'].items() %}
|
| 1884 |
<div class="product-row">
|
| 1885 |
{% if item['photo'] %}
|
| 1886 |
-
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ item['photo'] }}" style="width:
|
| 1887 |
{% else %}
|
| 1888 |
-
<div style="width:
|
| 1889 |
{% endif %}
|
| 1890 |
-
<span style="flex-grow:1; font-weight:
|
| 1891 |
<span style="font-weight:700;">{{ "%.2f"|format(item['price']|float * item['qty']|int) }} {{ currency }}</span>
|
| 1892 |
</div>
|
| 1893 |
{% endfor %}
|
|
@@ -1900,7 +1993,7 @@ ADMIN_ORDERS_TEMPLATE = '''
|
|
| 1900 |
{% else %}
|
| 1901 |
<div class="empty-state">
|
| 1902 |
<i class="fas fa-shopping-cart"></i>
|
| 1903 |
-
<p style="font-size: 1.
|
| 1904 |
</div>
|
| 1905 |
{% endif %}
|
| 1906 |
</div>
|
|
@@ -2228,7 +2321,7 @@ def admin(env_id):
|
|
| 2228 |
|
| 2229 |
data['products'] = new_products
|
| 2230 |
save_env_data(env_id, data)
|
| 2231 |
-
flash(f"Удалено
|
| 2232 |
|
| 2233 |
elif action == 'add_block':
|
| 2234 |
b_type = request.form.get('block_type')
|
|
@@ -2272,7 +2365,7 @@ def admin(env_id):
|
|
| 2272 |
})
|
| 2273 |
data['blocks'] = blocks
|
| 2274 |
save_env_data(env_id, data)
|
| 2275 |
-
flash("
|
| 2276 |
|
| 2277 |
elif action == 'edit_block':
|
| 2278 |
b_id = request.form.get('block_id')
|
|
@@ -2327,7 +2420,7 @@ def admin(env_id):
|
|
| 2327 |
break
|
| 2328 |
data['blocks'] = blocks
|
| 2329 |
save_env_data(env_id, data)
|
| 2330 |
-
flash("
|
| 2331 |
|
| 2332 |
elif action == 'delete_block':
|
| 2333 |
b_id = request.form.get('block_id')
|
|
@@ -2340,7 +2433,7 @@ def admin(env_id):
|
|
| 2340 |
blocks = [b for b in blocks if b.get('id') != b_id]
|
| 2341 |
data['blocks'] = blocks
|
| 2342 |
save_env_data(env_id, data)
|
| 2343 |
-
flash("
|
| 2344 |
|
| 2345 |
elif action == 'move_block_up':
|
| 2346 |
b_id = request.form.get('block_id')
|
|
@@ -2349,7 +2442,7 @@ def admin(env_id):
|
|
| 2349 |
blocks[idx], blocks[idx-1] = blocks[idx-1], blocks[idx]
|
| 2350 |
data['blocks'] = blocks
|
| 2351 |
save_env_data(env_id, data)
|
| 2352 |
-
flash("
|
| 2353 |
|
| 2354 |
elif action == 'move_block_down':
|
| 2355 |
b_id = request.form.get('block_id')
|
|
@@ -2358,7 +2451,7 @@ def admin(env_id):
|
|
| 2358 |
blocks[idx], blocks[idx+1] = blocks[idx+1], blocks[idx]
|
| 2359 |
data['blocks'] = blocks
|
| 2360 |
save_env_data(env_id, data)
|
| 2361 |
-
flash("
|
| 2362 |
|
| 2363 |
elif action == 'add_category':
|
| 2364 |
category_name = request.form.get('category_name', '').strip()
|
|
@@ -2366,11 +2459,11 @@ def admin(env_id):
|
|
| 2366 |
categories.append(category_name)
|
| 2367 |
data['categories'] = categories
|
| 2368 |
save_env_data(env_id, data)
|
| 2369 |
-
flash(f"
|
| 2370 |
elif not category_name:
|
| 2371 |
-
flash("Название
|
| 2372 |
else:
|
| 2373 |
-
flash(f"
|
| 2374 |
|
| 2375 |
elif action == 'delete_category':
|
| 2376 |
category_to_delete = request.form.get('category_name')
|
|
@@ -2382,7 +2475,7 @@ def admin(env_id):
|
|
| 2382 |
data['categories'] = categories
|
| 2383 |
data['products'] = products
|
| 2384 |
save_env_data(env_id, data)
|
| 2385 |
-
flash(f"
|
| 2386 |
|
| 2387 |
elif action == 'update_settings':
|
| 2388 |
settings['admin_password_enabled'] = 'admin_password_enabled' in request.form
|
|
@@ -2421,7 +2514,7 @@ def admin(env_id):
|
|
| 2421 |
except Exception: pass
|
| 2422 |
data['settings'] = settings
|
| 2423 |
save_env_data(env_id, data)
|
| 2424 |
-
flash("Настройки
|
| 2425 |
|
| 2426 |
elif action == 'edit_product':
|
| 2427 |
product_id = request.form.get('product_id')
|
|
@@ -2462,7 +2555,7 @@ def admin(env_id):
|
|
| 2462 |
|
| 2463 |
data['products'] = products
|
| 2464 |
save_env_data(env_id, data)
|
| 2465 |
-
flash("
|
| 2466 |
|
| 2467 |
elif action == 'delete_product':
|
| 2468 |
product_id = request.form.get('product_id')
|
|
@@ -2477,11 +2570,11 @@ def admin(env_id):
|
|
| 2477 |
except Exception: pass
|
| 2478 |
data['products'] = products
|
| 2479 |
save_env_data(env_id, data)
|
| 2480 |
-
flash("
|
| 2481 |
|
| 2482 |
return redirect(url_for('admin', env_id=env_id, p=page))
|
| 2483 |
except Exception as e:
|
| 2484 |
-
flash(f"
|
| 2485 |
return redirect(url_for('admin', env_id=env_id, p=page))
|
| 2486 |
|
| 2487 |
filtered_products = list(reversed(products))
|
|
@@ -2538,7 +2631,7 @@ def delete_order(env_id, order_id):
|
|
| 2538 |
orders = data.get('orders', [])
|
| 2539 |
data['orders'] = [o for o in orders if o.get('id') != order_id]
|
| 2540 |
save_env_data(env_id, data)
|
| 2541 |
-
flash("Заказ удален из истории.", "success")
|
| 2542 |
return redirect(url_for('admin_orders', env_id=env_id))
|
| 2543 |
|
| 2544 |
if __name__ == '__main__':
|
|
|
|
| 1 |
+
|
| 2 |
import os
|
| 3 |
import io
|
| 4 |
import base64
|
|
|
|
| 547 |
.cart-fab { position: fixed; bottom: 24px; right: 24px; background: var(--accent); color: var(--text-dark); width: 64px; height: 64px; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 1.6rem; cursor: pointer; box-shadow: 0 6px 20px rgba(0,0,0,0.3); z-index: 999; display: none; }
|
| 548 |
.cart-badge { position: absolute; top: -2px; right: -2px; background: var(--danger); color: white; border-radius: 50%; width: 26px; height: 26px; font-size: 0.85rem; display: flex; justify-content: center; align-items: center; font-weight: bold; border: 2px solid var(--bg-dark); }
|
| 549 |
|
| 550 |
+
.cart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; margin-top: 20px;}
|
| 551 |
+
.clear-cart-btn { background: transparent; color: var(--danger); border: 2px solid var(--danger); padding: 10px 15px; border-radius: 12px; font-weight: bold; cursor: pointer; font-size: 0.95rem; transition: 0.2s; }
|
| 552 |
+
.clear-cart-btn:hover { background: var(--danger); color: white; }
|
| 553 |
+
|
| 554 |
.cart-item { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid rgba(255,255,255,0.1); gap: 15px;}
|
| 555 |
+
.cart-item img { width: 75px; height: 75px; border-radius: 12px; object-fit: cover; transition: transform 0.3s; cursor: zoom-in; }
|
| 556 |
+
.cart-item img.zoomed {
|
| 557 |
+
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
| 558 |
+
width: 90vw !important; height: 90vh !important; object-fit: contain !important;
|
| 559 |
+
z-index: 99999; background: rgba(0,0,0,0.9); box-shadow: 0 0 0 100vmax rgba(0,0,0,0.9);
|
| 560 |
+
cursor: zoom-out; border-radius: 0 !important; border: none; padding: 20px;
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
.cart-item-info { flex-grow: 1; display:flex; flex-direction:column; justify-content:center; gap: 8px;}
|
| 564 |
+
.cart-item-price { font-size: 1.05rem; font-weight: bold; color: var(--accent); }
|
| 565 |
+
.cart-controls { display: flex; justify-content: space-between; align-items: center; background: var(--bg-medium); border-radius: 12px; overflow: hidden; height: 35px; width: 110px;}
|
| 566 |
+
.cart-controls button { background: var(--accent); color: var(--text-dark); border: none; width: 35px; height: 100%; font-weight: bold; cursor: pointer; font-size: 1.3rem; }
|
| 567 |
+
.cart-controls input { width: 40px; text-align: center; border: none; background: transparent; color: white; font-weight: bold; font-size: 1.1rem; outline: none; -moz-appearance: textfield; }
|
| 568 |
.cart-controls input::-webkit-outer-spin-button, .cart-controls input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
| 569 |
+
.cart-remove-btn { background: var(--danger); color: white; border: none; border-radius: 12px; width: 40px; height: 40px; font-size: 1.2rem; cursor: pointer; display: flex; justify-content: center; align-items: center; transition: 0.2s;}
|
| 570 |
+
.cart-remove-btn:hover { background: #ff4444; }
|
| 571 |
+
|
| 572 |
.add-to-cart-btn { background: var(--accent); color: var(--text-dark); border: none; padding: 12px; border-radius: 26px; font-weight: bold; cursor: pointer; width: 100%; font-size: 1.1rem; box-shadow: 0 4px 15px rgba(0,0,0,0.2); }
|
| 573 |
.add-to-cart-btn:hover { background: var(--accent-hover); }
|
| 574 |
|
|
|
|
| 672 |
<div id="cartModal" class="modal">
|
| 673 |
<div class="modal-content" style="padding: 24px; display: flex; flex-direction: column; min-height: 80vh; max-height: 95vh;">
|
| 674 |
<button class="close-btn" onclick="closeModal('cartModal')"><i class="fas fa-times"></i></button>
|
| 675 |
+
|
| 676 |
+
<div class="cart-header">
|
| 677 |
+
<h2 style="color: var(--accent); margin:0;">Корзина</h2>
|
| 678 |
+
<button onclick="clearCart()" class="clear-cart-btn" id="clearCartBtn"><i class="fas fa-trash-alt"></i> Очистить всё</button>
|
| 679 |
+
</div>
|
| 680 |
+
|
| 681 |
<div id="cartItems" style="flex-grow: 1; overflow-y: auto; padding-right: 5px;"></div>
|
| 682 |
+
|
| 683 |
<div id="cartTotal" style="font-size: 1.4rem; font-weight: bold; margin-top: 20px; text-align: right; color: var(--accent);"></div>
|
| 684 |
+
|
| 685 |
+
<div id="checkoutForm" style="margin-top: 25px; background: var(--card-bg); padding: 20px; border-radius: 20px; border: 1px solid rgba(255,255,255,0.1);">
|
| 686 |
+
<input type="text" id="customerName" placeholder="Ваше Имя" required style="width: 100%; padding: 16px; margin-bottom: 12px; border-radius: 14px; border: none; outline: none; background: #fff; color: #333; font-family: 'Montserrat', sans-serif; font-size: 1rem;">
|
| 687 |
+
<input type="tel" id="customerPhone" placeholder="Ваш Телефон" required style="width: 100%; padding: 16px; margin-bottom: 16px; border-radius: 14px; border: none; outline: none; background: #fff; color: #333; font-family: 'Montserrat', sans-serif; font-size: 1rem;">
|
| 688 |
<button onclick="submitOrder()" class="save-contact-btn" style="width: 100%; margin-bottom: 0; box-shadow: none;">Оформить заказ</button>
|
| 689 |
</div>
|
| 690 |
</div>
|
|
|
|
| 786 |
renderCartModal();
|
| 787 |
renderModalCartControls(productId);
|
| 788 |
}
|
| 789 |
+
|
| 790 |
+
function removeFromCart(productId) {
|
| 791 |
+
delete cart[productId];
|
| 792 |
+
saveCart();
|
| 793 |
+
updateCartBadge();
|
| 794 |
+
renderCatalog();
|
| 795 |
+
renderCartModal();
|
| 796 |
+
renderModalCartControls(productId);
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
function clearCart() {
|
| 800 |
+
if(confirm("Вы действительно хотите полностью очистить корзину?")) {
|
| 801 |
+
cart = {};
|
| 802 |
+
saveCart();
|
| 803 |
+
updateCartBadge();
|
| 804 |
+
renderCatalog();
|
| 805 |
+
renderCartModal();
|
| 806 |
+
}
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
function toggleCartZoom(imgElement) {
|
| 810 |
+
imgElement.classList.toggle('zoomed');
|
| 811 |
+
}
|
| 812 |
|
| 813 |
function setCategory(cat, btn) {
|
| 814 |
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
|
|
|
|
| 850 |
const paginated = filtered.slice(start, start + itemsPerPage);
|
| 851 |
|
| 852 |
if (filtered.length === 0) {
|
| 853 |
+
container.innerHTML = '<div class="no-results-message">В этой папке нет фото.</div>';
|
| 854 |
return;
|
| 855 |
}
|
| 856 |
|
|
|
|
| 986 |
function renderCartModal() {
|
| 987 |
const container = document.getElementById('cartItems');
|
| 988 |
const totalContainer = document.getElementById('cartTotal');
|
| 989 |
+
const clearBtn = document.getElementById('clearCartBtn');
|
| 990 |
+
const checkoutForm = document.getElementById('checkoutForm');
|
| 991 |
let html = '';
|
| 992 |
let total = 0;
|
| 993 |
+
|
| 994 |
for(let id in cart) {
|
| 995 |
let item = cart[id];
|
| 996 |
total += item.qty * item.price;
|
| 997 |
+
let photoUrl = item.photo ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}` : `https://via.placeholder.com/75x75.png?text=Фото`;
|
| 998 |
html += `
|
| 999 |
<div class="cart-item">
|
| 1000 |
+
<img src="${photoUrl}" alt="Фото" onclick="toggleCartZoom(this)">
|
| 1001 |
<div class="cart-item-info">
|
| 1002 |
<div class="cart-item-price">${item.price.toFixed(0)} ${currencyCode}</div>
|
| 1003 |
+
<div class="cart-controls">
|
| 1004 |
+
<button onclick="updateQty('${id}', -1)">-</button>
|
| 1005 |
+
<input type="number" value="${item.qty}" onchange="setQty('${id}', this)">
|
| 1006 |
+
<button onclick="updateQty('${id}', 1)">+</button>
|
| 1007 |
+
</div>
|
| 1008 |
</div>
|
| 1009 |
+
<button onclick="removeFromCart('${id}')" class="cart-remove-btn"><i class="fas fa-trash"></i></button>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1010 |
</div>
|
| 1011 |
`;
|
| 1012 |
}
|
| 1013 |
+
|
| 1014 |
if(html === '') {
|
| 1015 |
+
html = '<p style="text-align:center; opacity:0.7; font-size:1.1rem; margin-top:30px;">Ваша корзина пуста</p>';
|
| 1016 |
totalContainer.innerHTML = '';
|
| 1017 |
+
clearBtn.style.display = 'none';
|
| 1018 |
+
checkoutForm.style.display = 'none';
|
| 1019 |
} else {
|
| 1020 |
totalContainer.innerHTML = `Итого: ${total.toFixed(0)} ${currencyCode}`;
|
| 1021 |
+
clearBtn.style.display = 'block';
|
| 1022 |
+
checkoutForm.style.display = 'block';
|
| 1023 |
}
|
| 1024 |
container.innerHTML = html;
|
| 1025 |
}
|
|
|
|
| 1081 |
<meta charset="UTF-8">
|
| 1082 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 1083 |
<title>Настройки Визитки - {{ settings.organization_name }}</title>
|
| 1084 |
+
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 1085 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 1086 |
<style>
|
| 1087 |
:root { --bg-light: #f4f6f9; --bg-medium: #135D66; --accent: #48D1CC; --accent-hover: #77E4D8; --text-dark: #333; --text-on-accent: #003C43; --danger: #E57373; --danger-hover: #EF5350; }
|
| 1088 |
* { box-sizing: border-box; }
|
| 1089 |
+
body { font-family: 'Montserrat', sans-serif; background-color: var(--bg-light); color: var(--text-dark); padding: 20px; line-height: 1.6; margin: 0; font-size: 1.1rem; }
|
| 1090 |
+
|
| 1091 |
+
.container { max-width: 1200px; margin: 0 auto; background-color: #fff; padding: 25px; border-radius: 24px; box-shadow: 0 10px 40px rgba(0,0,0,0.04); }
|
| 1092 |
+
|
| 1093 |
+
/* Header updates for mobile */
|
| 1094 |
+
.header { padding-bottom: 20px; margin-bottom: 30px; border-bottom: 1px solid #f0f0f0; display: flex; flex-direction: column; align-items: center; text-align: center; gap: 15px;}
|
| 1095 |
+
.header .logo-title-container { display: flex; flex-direction: column; align-items: center; gap: 10px; }
|
| 1096 |
+
.header .logo-title-container img { height: 70px; width: 70px; border-radius: 50%; object-fit: cover; border: 3px solid var(--bg-medium);}
|
| 1097 |
+
.header-actions { display: flex; flex-direction: column; width: 100%; gap: 10px; margin-top: 10px; }
|
| 1098 |
+
|
| 1099 |
+
@media (min-width: 768px) {
|
| 1100 |
+
.header { flex-direction: row; justify-content: space-between; text-align: left; }
|
| 1101 |
+
.header .logo-title-container { flex-direction: row; }
|
| 1102 |
+
.header-actions { flex-direction: row; width: auto; margin-top: 0; }
|
| 1103 |
+
}
|
| 1104 |
+
|
| 1105 |
+
h1, h2, h3 { font-weight: 700; color: var(--bg-medium); margin-bottom: 15px; }
|
| 1106 |
+
h1 { font-size: 1.8rem; margin: 0; }
|
| 1107 |
h2 { font-size: 1.5rem; margin-top: 20px; display: flex; align-items: center; gap: 10px; }
|
| 1108 |
+
h3 { font-size: 1.3rem; color: #004D40; margin-top: 20px; }
|
| 1109 |
+
|
| 1110 |
+
.section { margin-bottom: 30px; padding: 20px; background-color: #fff; border: 2px solid #f0f0f0; border-radius: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.02); }
|
| 1111 |
+
|
| 1112 |
+
label { font-weight: 700; margin-top: 15px; display: block; color: #222; font-size: 1.1rem;}
|
| 1113 |
+
input[type="text"], input[type="number"], input[type="password"], input[type="tel"], textarea, select {
|
| 1114 |
+
width: 100%; padding: 16px; margin-top: 8px; border: 2px solid #ccc; background: #fff;
|
| 1115 |
+
border-radius: 16px; font-size: 1.15rem; box-sizing: border-box; transition: all 0.3s ease; min-height: 60px;
|
| 1116 |
+
}
|
| 1117 |
+
input:focus, textarea:focus, select:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 4px rgba(72, 209, 204, 0.15); }
|
| 1118 |
+
textarea { min-height: 120px; resize: vertical; }
|
| 1119 |
+
input[type="file"] { padding: 15px; background-color: #f8f9fa; cursor: pointer; border: 2px dashed #ccc; border-radius: 16px; }
|
| 1120 |
+
input[type="checkbox"] { margin-right: 12px; vertical-align: middle; width: 28px; height: 28px; cursor: pointer; accent-color: var(--bg-medium);}
|
| 1121 |
+
|
| 1122 |
+
/* Big readable buttons */
|
| 1123 |
+
button, .button {
|
| 1124 |
+
padding: 16px 20px; border: none; border-radius: 16px; background-color: var(--accent);
|
| 1125 |
+
color: var(--text-on-accent); font-weight: 700; cursor: pointer; transition: 0.2s ease;
|
| 1126 |
+
margin-top: 15px; font-size: 1.2rem; display: inline-flex; align-items: center; justify-content: center;
|
| 1127 |
+
gap: 10px; text-decoration: none; min-height: 60px; width: 100%;
|
| 1128 |
+
}
|
| 1129 |
+
@media (min-width: 768px) { button, .button { width: auto; } }
|
| 1130 |
button:hover, .button:hover { background-color: var(--accent-hover); }
|
| 1131 |
button:active, .button:active { transform: scale(0.98); }
|
| 1132 |
+
|
| 1133 |
.delete-button { background-color: var(--danger); color: white; }
|
| 1134 |
.delete-button:hover { background-color: var(--danger-hover); }
|
| 1135 |
.add-button { background-color: var(--bg-medium); color: white; }
|
| 1136 |
.add-button:hover { background-color: #003C43; }
|
| 1137 |
+
|
| 1138 |
.item-list { display: grid; gap: 20px; }
|
| 1139 |
+
.item { background: #fff; padding: 25px; border-radius: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); border: 2px solid #f0f0f0; }
|
| 1140 |
+
.item p { margin: 8px 0; font-size: 1.1rem; color: #444; }
|
|
|
|
| 1141 |
.item strong { color: var(--text-dark); }
|
| 1142 |
+
.item-actions { margin-top: 20px; display: flex; flex-direction: column; gap: 10px; }
|
| 1143 |
+
@media (min-width: 768px) { .item-actions { flex-direction: row; } }
|
| 1144 |
+
|
| 1145 |
+
.edit-form-container { margin-top: 25px; padding: 25px; background: #f8f9fa; border: 2px solid #e9ecef; border-radius: 20px; display: none; }
|
| 1146 |
+
|
| 1147 |
+
/* Details as big interactive cards */
|
| 1148 |
+
details { background-color: #fff; border: 2px solid #f0f0f0; border-radius: 20px; margin-bottom: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.03); }
|
| 1149 |
+
details > summary { cursor: pointer; font-weight: 700; color: var(--bg-medium); display: block; padding: 25px 20px; list-style: none; position: relative; font-size: 1.3rem; }
|
| 1150 |
+
details > summary:hover { background-color: #f8f9fa; border-radius: 20px;}
|
| 1151 |
details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 24px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; color: var(--bg-medium); }
|
| 1152 |
details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
|
| 1153 |
+
details[open] > summary { border-bottom: 2px solid #f0f0f0; border-radius: 20px 20px 0 0; }
|
| 1154 |
+
details .form-content { padding: 25px; }
|
| 1155 |
+
|
| 1156 |
+
.photo-preview { display: flex; gap: 10px; flex-wrap: wrap; }
|
| 1157 |
+
.photo-preview img { width: 120px; height: 120px; border-radius: 16px; border: 2px solid #e0e0e0; object-fit: cover;}
|
| 1158 |
+
|
| 1159 |
+
.message { padding: 20px; border-radius: 16px; margin-bottom: 25px; font-size: 1.15rem; font-weight: 600;}
|
| 1160 |
+
.message.success { background-color: #d4edda; color: #155724; border: 2px solid #c3e6cb;}
|
| 1161 |
+
.message.error { background-color: #f8d7da; color: #721c24; border: 2px solid #f5c6cb;}
|
| 1162 |
+
|
| 1163 |
+
.current-avatar { max-width: 80px; max-height: 80px; border-radius: 50%; vertical-align: middle; margin-left: 15px; border: 3px solid var(--bg-medium);}
|
| 1164 |
+
|
| 1165 |
+
.block-item { display: flex; flex-direction: column; background: #fff; padding: 25px; border: 2px solid #f0f0f0; border-radius: 20px; margin-bottom: 15px; gap: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.03);}
|
| 1166 |
+
@media (min-width: 768px) { .block-item { flex-direction: row; justify-content: space-between; align-items: center; } }
|
| 1167 |
+
|
| 1168 |
+
.block-controls { display: flex; flex-direction: column; gap: 10px; width: 100%; }
|
| 1169 |
+
@media (min-width: 768px) { .block-controls { flex-direction: row; width: auto; } }
|
| 1170 |
+
|
| 1171 |
+
.btn-small { padding: 12px; font-size: 1.1rem; min-height: 50px;}
|
| 1172 |
+
|
| 1173 |
+
.pagination { display: flex; justify-content: center; gap: 10px; margin-top: 40px; flex-wrap: wrap; align-items: center; }
|
| 1174 |
+
.pagination .button { min-width: 60px; text-align: center; padding: 15px; margin: 0; border-radius: 16px; width: auto; }
|
| 1175 |
+
|
| 1176 |
#loadingOverlay {
|
| 1177 |
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
| 1178 |
background: rgba(0,0,0,0.85); backdrop-filter: blur(5px); z-index: 10000; flex-direction: column;
|
| 1179 |
justify-content: center; align-items: center; color: white; text-align: center;
|
| 1180 |
}
|
| 1181 |
.spinner {
|
| 1182 |
+
width: 70px; height: 70px; border: 6px solid rgba(255,255,255,0.2);
|
| 1183 |
border-top: 6px solid var(--accent); border-radius: 50%;
|
| 1184 |
animation: spin 1s linear infinite; margin-bottom: 25px;
|
| 1185 |
}
|
| 1186 |
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
|
|
|
|
|
|
|
|
| 1187 |
</style>
|
| 1188 |
</head>
|
| 1189 |
<body>
|
| 1190 |
<div id="loadingOverlay">
|
| 1191 |
<div class="spinner"></div>
|
| 1192 |
+
<h2 style="color:white; margin:0;">Пожалуйста, подождите...<br>Идёт сохранение данных</h2>
|
| 1193 |
</div>
|
| 1194 |
<div class="container">
|
| 1195 |
<div class="header">
|
| 1196 |
+
<div class="logo-title-container">
|
| 1197 |
<img src="{{ chat_avatar_url }}" alt="Logo">
|
| 1198 |
+
<h1 style="margin:0;"><i class="fas fa-id-badge"></i> Настройки сайта</h1>
|
| 1199 |
</div>
|
| 1200 |
+
<div class="header-actions">
|
| 1201 |
+
<a href="{{ url_for('catalog', env_id=env_id) }}" class="button" style="background-color: var(--bg-medium); color: white;"><i class="fas fa-external-link-alt"></i> Открыть сайт</a>
|
| 1202 |
<a href="{{ url_for('admin_orders', env_id=env_id) }}" class="button" style="background-color: {% if new_orders_count > 0 %}#E57373{% else %}var(--bg-medium){% endif %}; color: white; position: relative;">
|
| 1203 |
<i class="fas fa-history"></i> Заказы
|
| 1204 |
{% if new_orders_count > 0 %}
|
| 1205 |
+
<span style="background-color: white; color: #E57373; border-radius: 50%; padding: 4px 10px; font-size: 1rem; font-weight: bold; margin-left: 8px; border: 2px solid #E57373;">{{ new_orders_count }}</span>
|
| 1206 |
{% endif %}
|
| 1207 |
</a>
|
| 1208 |
+
<button onclick="downloadQR()" class="button" style="background-color: #8A2BE2; color: white;"><i class="fas fa-qrcode"></i> Сохранить QR</button>
|
| 1209 |
{% if settings.admin_password_enabled %}
|
| 1210 |
+
<a href="{{ url_for('admin_logout', env_id=env_id) }}" class="button" style="background-color: #6c757d; color: white;"><i class="fas fa-sign-out-alt"></i> Выйти</a>
|
| 1211 |
{% endif %}
|
| 1212 |
</div>
|
| 1213 |
</div>
|
|
|
|
| 1238 |
</div>
|
| 1239 |
</div>
|
| 1240 |
|
| 1241 |
+
<label>Профессия / Должность:</label>
|
| 1242 |
<input type="text" name="vcard_job" value="{{ settings.vcard_job }}">
|
| 1243 |
|
| 1244 |
<label>Название организации:</label>
|
| 1245 |
<input type="text" name="organization_name" value="{{ settings.organization_name }}">
|
| 1246 |
|
| 1247 |
+
<label>Коротко о себе (описание):</label>
|
| 1248 |
<textarea name="about_text" rows="3">{{ settings.about_text }}</textarea>
|
| 1249 |
|
| 1250 |
+
<label>Ваша фотография (Аватар):</label>
|
| 1251 |
<input type="file" name="chat_avatar" accept="image/*">
|
| 1252 |
{% if settings.chat_avatar %}
|
| 1253 |
+
<p style="font-size: 1.1rem; margin-top: 15px; display: flex; align-items: center;">Текущая: <img src="{{ chat_avatar_url }}" class="current-avatar"></p>
|
| 1254 |
{% endif %}
|
| 1255 |
|
| 1256 |
+
<label>Дизайн (Цветовая схема):</label>
|
| 1257 |
<select name="color_scheme">
|
| 1258 |
{% for key, name in color_schemes.items() %}
|
| 1259 |
<option value="{{ key }}" {% if settings.color_scheme == key %}selected{% endif %}>{{ name }}</option>
|
| 1260 |
{% endfor %}
|
| 1261 |
</select>
|
| 1262 |
|
| 1263 |
+
<label>Валюта цен на сайте:</label>
|
| 1264 |
<select name="currency_code">
|
| 1265 |
{% for code, name in currencies.items() %}
|
| 1266 |
<option value="{{ code }}" {% if settings.currency_code == code %}selected{% endif %}>{{ name }} ({{ code }})</option>
|
| 1267 |
{% endfor %}
|
| 1268 |
</select>
|
| 1269 |
|
| 1270 |
+
<h4 style="margin-top: 40px; font-size:1.3rem; color:var(--bg-medium);"><i class="fas fa-shopping-cart"></i> Прием заказов с сайта</h4>
|
| 1271 |
+
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; padding: 10px 0;">
|
| 1272 |
<input type="checkbox" name="enable_cart" {% if settings.enable_cart %}checked{% endif %}>
|
| 1273 |
+
Включить возможность заказа товаров
|
| 1274 |
</label>
|
| 1275 |
+
<label>Куда получать заказы:</label>
|
| 1276 |
<select name="order_messenger">
|
| 1277 |
+
<option value="whatsapp" {% if settings.order_messenger == 'whatsapp' %}selected{% endif %}>На WhatsApp</option>
|
| 1278 |
+
<option value="telegram" {% if settings.order_messenger == 'telegram' %}selected{% endif %}>На Telegram</option>
|
| 1279 |
<option value="none" {% if settings.order_messenger == 'none' %}selected{% endif %}>Не принимать заказы</option>
|
| 1280 |
</select>
|
| 1281 |
+
<label>Ваш номер (для WhatsApp) или логин (для Telegram):</label>
|
| 1282 |
+
<input type="text" name="order_contact" value="{{ settings.order_contact }}" placeholder="Например: 996555123456 или my_login">
|
| 1283 |
|
| 1284 |
+
<div style="background: #f1f3f5; padding: 25px; border-radius: 20px; margin-top: 40px; border:2px solid #e9ecef;">
|
| 1285 |
+
<h4 style="margin-top: 0; color: var(--bg-medium); font-size: 1.3rem;"><i class="fas fa-lock"></i> Пароль для входа сюда</h4>
|
| 1286 |
+
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; padding: 10px 0;"><input type="checkbox" name="admin_password_enabled" {% if settings.admin_password_enabled %}checked{% endif %}> Включить запрос пароля</label>
|
| 1287 |
<label style="margin-top: 15px;">Пароль:</label>
|
| 1288 |
<input type="text" name="admin_password" value="{{ settings.admin_password }}" placeholder="Текущий пароль">
|
| 1289 |
</div>
|
| 1290 |
|
| 1291 |
+
<button type="submit" class="add-button" style="margin-top: 30px;"><i class="fas fa-save"></i> Сохранить настройки</button>
|
| 1292 |
</form>
|
| 1293 |
</div>
|
| 1294 |
</details>
|
| 1295 |
</div>
|
| 1296 |
|
| 1297 |
<div class="section">
|
| 1298 |
+
<h2><i class="fas fa-link"></i> Кнопки и Ссылки</h2>
|
| 1299 |
<details>
|
| 1300 |
+
<summary><i class="fas fa-plus-circle"></i> Создать новую кнопку</summary>
|
| 1301 |
<div class="form-content">
|
| 1302 |
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" enctype="multipart/form-data" onsubmit="showLoadingOverlay()">
|
| 1303 |
<input type="hidden" name="action" value="add_block">
|
| 1304 |
+
<label>Тип кнопки:</label>
|
| 1305 |
<select name="block_type" id="add_block_type" onchange="toggleBlockFields('add_')">
|
| 1306 |
+
<option value="link">Обычная ссылка/номер</option>
|
| 1307 |
+
<option value="text">Текст (описание)</option>
|
| 1308 |
+
<option value="pdf">Открыть PDF файл</option>
|
| 1309 |
</select>
|
| 1310 |
+
<label>Надпись на кнопке (заголовок):</label>
|
| 1311 |
<input type="text" name="block_title" required>
|
| 1312 |
|
| 1313 |
<div id="add_block_icon_div">
|
| 1314 |
+
<label>Иконка на кнопке:</label>
|
| 1315 |
<select name="block_icon">
|
| 1316 |
<option value="">-- Без иконки --</option>
|
| 1317 |
{% for icon_class, icon_name in icons.items() %}
|
|
|
|
| 1321 |
</div>
|
| 1322 |
|
| 1323 |
<div id="add_block_url_div">
|
| 1324 |
+
<label>Ссылка или номер телефона (с +):</label>
|
| 1325 |
+
<input type="text" name="block_url" placeholder="https://... или +996...">
|
| 1326 |
</div>
|
| 1327 |
<div id="add_block_pdf_div" style="display: none;">
|
| 1328 |
+
<label>Выберите PDF файл с телефона:</label>
|
| 1329 |
<input type="file" name="block_pdf" accept="application/pdf">
|
| 1330 |
</div>
|
| 1331 |
<div id="add_block_content_div" style="display: none;">
|
| 1332 |
+
<label>Текст сообщения:</label>
|
| 1333 |
<textarea name="block_content" rows="3"></textarea>
|
| 1334 |
</div>
|
| 1335 |
+
<button type="submit" class="add-button"><i class="fas fa-plus"></i> Добавить кнопку</button>
|
| 1336 |
</form>
|
| 1337 |
</div>
|
| 1338 |
</details>
|
| 1339 |
+
<div style="margin-top: 20px;">
|
| 1340 |
{% if blocks %}
|
| 1341 |
{% for block in blocks %}
|
| 1342 |
<div class="block-item">
|
| 1343 |
<div style="flex-grow: 1;">
|
| 1344 |
+
<strong style="font-size: 1.25rem;">
|
| 1345 |
{% if block.icon %}
|
| 1346 |
{% set prefix = 'fab' if block.icon in ['fa-whatsapp', 'fa-telegram', 'fa-instagram', 'fa-youtube', 'fa-tiktok'] else 'fas' %}
|
| 1347 |
<i class="{{ prefix }} {{ block.icon }}" style="color:var(--accent);"></i>
|
| 1348 |
{% endif %}
|
| 1349 |
{{ block.title }}
|
| 1350 |
</strong>
|
| 1351 |
+
<span style="color: #666; font-size: 1rem; margin-left: 10px; display:inline-block; margin-top:5px;">
|
| 1352 |
({% if block.type == 'link' %}Ссылка{% elif block.type == 'pdf' %}PDF{% else %}Текст{% endif %})
|
| 1353 |
</span>
|
| 1354 |
+
{% if block.type == 'link' %}<br><small style="font-size: 1.05rem; color: #444; margin-top: 8px; display: block; word-break:break-all;"><a href="{{ block.url }}" target="_blank" rel="noopener noreferrer">{{ block.url }}</a></small>{% endif %}
|
| 1355 |
</div>
|
| 1356 |
<div class="block-controls">
|
| 1357 |
+
<button type="button" class="button btn-small" style="background: #1E90FF;" onclick="toggleEditForm('edit-block-{{ block.id }}'); toggleBlockFields('edit_{{ block.id }}_');"><i class="fas fa-edit"></i> Редактировать</button>
|
| 1358 |
+
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" style="margin: 0; width:100%;">
|
| 1359 |
<input type="hidden" name="action" value="move_block_up">
|
| 1360 |
<input type="hidden" name="block_id" value="{{ block.id }}">
|
| 1361 |
+
<button type="submit" class="button btn-small" style="background: #6c757d;" {% if loop.first %}disabled{% endif %}><i class="fas fa-arrow-up"></i> Выше</button>
|
| 1362 |
</form>
|
| 1363 |
+
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" style="margin: 0; width:100%;">
|
| 1364 |
<input type="hidden" name="action" value="move_block_down">
|
| 1365 |
<input type="hidden" name="block_id" value="{{ block.id }}">
|
| 1366 |
+
<button type="submit" class="button btn-small" style="background: #6c757d;" {% if loop.last %}disabled{% endif %}><i class="fas fa-arrow-down"></i> Ниже</button>
|
| 1367 |
</form>
|
| 1368 |
+
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" style="margin: 0; width:100%;" onsubmit="if(!confirm('Удалить эту кнопку?')) return false; showLoadingOverlay(); return true;">
|
| 1369 |
<input type="hidden" name="action" value="delete_block">
|
| 1370 |
<input type="hidden" name="block_id" value="{{ block.id }}">
|
| 1371 |
+
<button type="submit" class="button delete-button btn-small"><i class="fas fa-trash-alt"></i> Удалить</button>
|
| 1372 |
</form>
|
| 1373 |
</div>
|
| 1374 |
</div>
|
|
|
|
| 1378 |
<input type="hidden" name="block_id" value="{{ block.id }}">
|
| 1379 |
<input type="hidden" name="old_block_url" value="{{ block.url }}">
|
| 1380 |
|
| 1381 |
+
<label>Тип кнопки:</label>
|
| 1382 |
<select name="block_type" id="edit_{{ block.id }}_block_type" onchange="toggleBlockFields('edit_{{ block.id }}_')">
|
| 1383 |
+
<option value="link" {% if block.type == 'link' %}selected{% endif %}>Обычная ссылка/номер</option>
|
| 1384 |
+
<option value="text" {% if block.type == 'text' %}selected{% endif %}>Текст (описание)</option>
|
| 1385 |
+
<option value="pdf" {% if block.type == 'pdf' %}selected{% endif %}>Открыть PDF файл</option>
|
| 1386 |
</select>
|
| 1387 |
+
<label>Надпись на кнопке (заголовок):</label>
|
| 1388 |
<input type="text" name="block_title" value="{{ block.title }}" required>
|
| 1389 |
|
| 1390 |
<div id="edit_{{ block.id }}_block_icon_div">
|
|
|
|
| 1398 |
</div>
|
| 1399 |
|
| 1400 |
<div id="edit_{{ block.id }}_block_url_div">
|
| 1401 |
+
<label>Ссылка или номер телефона:</label>
|
| 1402 |
<input type="text" name="block_url" value="{{ block.url if block.type == 'link' else '' }}">
|
| 1403 |
</div>
|
| 1404 |
<div id="edit_{{ block.id }}_block_pdf_div" style="display: none;">
|
| 1405 |
<label>Текущий PDF: {% if block.type == 'pdf' and block.url %}{{ block.url }}{% else %}Нет{% endif %}</label>
|
| 1406 |
+
<label>Загрузить новый PDF (старый удалится):</label>
|
| 1407 |
<input type="file" name="block_pdf" accept="application/pdf">
|
| 1408 |
</div>
|
| 1409 |
<div id="edit_{{ block.id }}_block_content_div" style="display: none;">
|
| 1410 |
+
<label>Текст сообщения:</label>
|
| 1411 |
<textarea name="block_content" rows="3">{{ block.content }}</textarea>
|
| 1412 |
</div>
|
| 1413 |
+
<button type="submit" class="add-button"><i class="fas fa-save"></i> Сохранить изменения</button>
|
| 1414 |
</form>
|
| 1415 |
</div>
|
| 1416 |
{% endfor %}
|
| 1417 |
{% else %}
|
| 1418 |
+
<p style="font-size: 1.15rem; color:#888;">Кнопки еще не добавлены.</p>
|
| 1419 |
{% endif %}
|
| 1420 |
</div>
|
| 1421 |
<script>
|
|
|
|
| 1440 |
</div>
|
| 1441 |
|
| 1442 |
<div class="section">
|
| 1443 |
+
<h2><i class="fas fa-folder-open"></i> Папки (для группировки фото/товаров)</h2>
|
| 1444 |
<details>
|
| 1445 |
+
<summary><i class="fas fa-plus-circle"></i> Создать новую папку</summary>
|
| 1446 |
<div class="form-content">
|
| 1447 |
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}">
|
| 1448 |
<input type="hidden" name="action" value="add_category">
|
| 1449 |
+
<label>Название новой папки:</label>
|
| 1450 |
<input type="text" name="category_name" required>
|
| 1451 |
+
<button type="submit" class="add-button"><i class="fas fa-plus"></i> Создать папку</button>
|
| 1452 |
</form>
|
| 1453 |
</div>
|
| 1454 |
</details>
|
| 1455 |
{% if categories %}
|
| 1456 |
+
<div class="item-list" style="margin-top:25px;">
|
| 1457 |
{% for category in categories %}
|
| 1458 |
+
<div class="item" style="display: flex; justify-content: space-between; align-items: center; flex-wrap:wrap; gap:15px;">
|
| 1459 |
+
<span style="font-size: 1.25rem; font-weight: 700; color: var(--bg-medium);">{{ category }}</span>
|
| 1460 |
+
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" style="margin: 0; width:100%;" onsubmit="if(!confirm('Точно удалить папку? Все фото из неё останутся, но будут \\'Без папки\\'.')) return false;">
|
| 1461 |
<input type="hidden" name="action" value="delete_category">
|
| 1462 |
<input type="hidden" name="category_name" value="{{ category }}">
|
| 1463 |
+
<button type="submit" class="delete-button" style="margin: 0;"><i class="fas fa-trash-alt"></i> Удалить папку</button>
|
| 1464 |
</form>
|
| 1465 |
</div>
|
| 1466 |
{% endfor %}
|
| 1467 |
</div>
|
| 1468 |
{% else %}
|
| 1469 |
+
<p style="font-size: 1.15rem; margin-top:20px; color:#888;">Папок пока нет.</p>
|
| 1470 |
{% endif %}
|
| 1471 |
</div>
|
| 1472 |
|
| 1473 |
<div class="section">
|
| 1474 |
+
<h2><i class="fas fa-camera"></i> Все фотографии и товары</h2>
|
| 1475 |
<details open>
|
| 1476 |
+
<summary><i class="fas fa-upload"></i> Загрузить много фото на сайт</summary>
|
| 1477 |
<div class="form-content">
|
| 1478 |
+
<label>В какую папку добавить фото:</label>
|
| 1479 |
<select id="bulk_category">
|
| 1480 |
+
<option value="Без категории">Без папки</option>
|
| 1481 |
{% for category in categories %}
|
| 1482 |
<option value="{{ category }}">{{ category }}</option>
|
| 1483 |
{% endfor %}
|
| 1484 |
</select>
|
| 1485 |
|
| 1486 |
+
<label style="margin-top: 20px;">Выберите фото с телефона (можно выбрать сразу несколько):</label>
|
| 1487 |
<input type="file" id="bulk_photos" accept="image/*" multiple onchange="handleBulkSelect(event)">
|
| 1488 |
|
| 1489 |
+
<div id="bulk_preview" style="display: flex; flex-wrap: wrap; gap: 15px; margin-top: 25px;"></div>
|
| 1490 |
|
| 1491 |
+
<div id="progress_container" style="display:none; margin-top:30px;">
|
| 1492 |
+
<div style="width:100%; background:#eee; border-radius:16px; overflow:hidden;">
|
| 1493 |
+
<div id="progress_bar" style="width:0%; height:30px; background:var(--accent); transition:width 0.2s;"></div>
|
| 1494 |
</div>
|
| 1495 |
+
<p id="progress_text" style="text-align:center; font-weight:bold; margin-top:10px; font-size:1.2rem;">0%</p>
|
| 1496 |
</div>
|
| 1497 |
|
| 1498 |
+
<button type="button" class="add-button" style="margin-top: 30px; width:100%; font-size:1.2rem; min-height:65px;" onclick="uploadBulk()"><i class="fas fa-cloud-upload-alt"></i> Опубликовать фото на сайте</button>
|
| 1499 |
</div>
|
| 1500 |
</details>
|
| 1501 |
|
| 1502 |
+
<h3 style="margin-top: 45px; margin-bottom: 25px;">Загруженные фотографии:</h3>
|
| 1503 |
{% if paginated_products %}
|
| 1504 |
<div class="item-list" id="admin-products-list">
|
| 1505 |
{% for product in paginated_products %}
|
| 1506 |
<div class="item">
|
| 1507 |
+
<div style="display: flex; gap: 20px; align-items: flex-start; flex-direction:column;">
|
| 1508 |
+
<div style="display: flex; align-items: center; gap: 15px;">
|
| 1509 |
+
<input type="checkbox" class="bulk-delete-checkbox" value="{{ product.product_id }}" onchange="updateBulkDeleteFab()">
|
| 1510 |
+
<span style="font-weight:700; color:#555;">Отметить</span>
|
| 1511 |
+
</div>
|
| 1512 |
+
|
| 1513 |
+
<div class="photo-preview" style="display:flex; flex-wrap:wrap; width: 100%;">
|
| 1514 |
{% if product.get('photos') %}
|
| 1515 |
{% for p in product['photos'] %}
|
| 1516 |
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ p }}" alt="Фото">
|
| 1517 |
{% endfor %}
|
| 1518 |
{% else %}
|
| 1519 |
+
<img src="https://via.placeholder.com/120x120.png?text=Нет+фото" alt="Нет фото">
|
| 1520 |
{% endif %}
|
| 1521 |
</div>
|
| 1522 |
+
|
| 1523 |
+
<div style="flex-grow: 1; width: 100%;">
|
| 1524 |
+
<p style="font-size: 1.15rem;"><strong>Папка:</strong> {{ product.get('category', 'Без папки') if product.get('category') != 'Без категории' else 'Без папки' }}</p>
|
| 1525 |
+
<p style="font-size: 1.25rem; color: var(--bg-medium);"><strong>Цена:</strong> {% if product.get('price', 0) > 0 %}{{ "%.2f"|format(product.get('price', 0)) }} {{ currency_code }}{% else %}0{% endif %}</p>
|
| 1526 |
</div>
|
| 1527 |
</div>
|
| 1528 |
|
| 1529 |
<div class="item-actions">
|
| 1530 |
+
<button type="button" class="button" onclick="toggleEditForm('edit-form-{{ product.product_id }}')"><i class="fas fa-edit"></i> Изменить</button>
|
| 1531 |
+
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" style="margin:0; width:100%;" onsubmit="if(!confirm('Точно удалить это фото/товар?')) return false; showLoadingOverlay(); return true;">
|
| 1532 |
<input type="hidden" name="action" value="delete_product">
|
| 1533 |
<input type="hidden" name="product_id" value="{{ product.get('product_id', '') }}">
|
| 1534 |
+
<button type="submit" class="delete-button button"><i class="fas fa-trash-alt"></i> Удалить</button>
|
| 1535 |
</form>
|
| 1536 |
</div>
|
| 1537 |
|
| 1538 |
<div id="edit-form-{{ product.product_id }}" class="edit-form-container">
|
| 1539 |
+
<h4 style="margin-top: 0; font-size: 1.25rem;"><i class="fas fa-edit"></i> Изменение товара</h4>
|
| 1540 |
<form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" enctype="multipart/form-data" onsubmit="showLoadingOverlay()">
|
| 1541 |
<input type="hidden" name="action" value="edit_product">
|
| 1542 |
<input type="hidden" name="product_id" value="{{ product.get('product_id', '') }}">
|
| 1543 |
|
| 1544 |
+
<label>Новая цена:</label>
|
| 1545 |
<input type="number" name="price" step="0.01" value="{{ product.get('price', 0) }}">
|
| 1546 |
|
| 1547 |
+
<label>Перенести в папку:</label>
|
| 1548 |
<select name="category">
|
| 1549 |
+
<option value="Без категории">Без папки</option>
|
| 1550 |
{% for category in categories %}
|
| 1551 |
<option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
|
| 1552 |
{% endfor %}
|
| 1553 |
</select>
|
| 1554 |
|
| 1555 |
+
<label>Загрузить другие фото вместо старых:</label>
|
| 1556 |
<input type="file" name="photos" accept="image/*" multiple>
|
| 1557 |
|
| 1558 |
+
<button type="submit" class="add-button" style="margin-top: 25px;"><i class="fas fa-save"></i> Сохранить изменения</button>
|
| 1559 |
</form>
|
| 1560 |
</div>
|
| 1561 |
</div>
|
|
|
|
| 1565 |
{% if total_pages > 1 %}
|
| 1566 |
<div class="pagination">
|
| 1567 |
{% if page > 1 %}
|
| 1568 |
+
<a href="{{ url_for('admin', env_id=env_id, p=page-1) }}" class="button" style="padding:15px; min-width:auto;"><i class="fas fa-chevron-left"></i> Назад</a>
|
| 1569 |
{% endif %}
|
| 1570 |
|
| 1571 |
{% for p_num in range(1, total_pages + 1) %}
|
| 1572 |
{% if p_num == 1 or p_num == total_pages or (p_num >= page - 2 and p_num <= page + 2) %}
|
| 1573 |
+
<a href="{{ url_for('admin', env_id=env_id, p=p_num) }}" class="button {% if p_num == page %}active{% endif %}" style="padding:15px; min-width:50px; {% if p_num == page %}background-color: var(--accent); color: var(--text-dark);{% else %}background-color: var(--bg-medium); color: white;{% endif %}">{{ p_num }}</a>
|
| 1574 |
{% elif p_num == page - 3 or p_num == page + 3 %}
|
| 1575 |
+
<span style="padding: 15px; color: var(--bg-medium); font-weight: bold; font-size:1.2rem;">...</span>
|
| 1576 |
{% endif %}
|
| 1577 |
{% endfor %}
|
| 1578 |
|
| 1579 |
{% if page < total_pages %}
|
| 1580 |
+
<a href="{{ url_for('admin', env_id=env_id, p=page+1) }}" class="button" style="padding:15px; min-width:auto;">Вперед <i class="fas fa-chevron-right"></i></a>
|
| 1581 |
{% endif %}
|
| 1582 |
</div>
|
| 1583 |
{% endif %}
|
| 1584 |
|
| 1585 |
{% else %}
|
| 1586 |
+
<p style="font-size: 1.25rem; text-align: center; padding: 50px; color:#888;">Фотографии пока не добавлены.</p>
|
| 1587 |
{% endif %}
|
| 1588 |
</div>
|
| 1589 |
</div>
|
|
|
|
| 1593 |
<div id="bulk-delete-inputs"></div>
|
| 1594 |
</form>
|
| 1595 |
|
| 1596 |
+
<button id="bulk-delete-fab" class="button delete-button" style="display:none; position:fixed; bottom:24px; left:24px; z-index:999; border-radius:24px; box-shadow:0 6px 20px rgba(0,0,0,0.4); font-size:1.2rem; padding:18px 30px;" type="button" onclick="submitBulkDelete()">
|
| 1597 |
+
<i class="fas fa-trash-alt"></i> Удалить выбранное (<span id="bulk-delete-count">0</span>)
|
| 1598 |
</button>
|
| 1599 |
|
| 1600 |
+
<button id="carousel-fab" class="button" style="display:none; position:fixed; bottom:24px; right:24px; z-index:999; border-radius:24px; box-shadow:0 6px 20px rgba(0,0,0,0.4); font-size:1.2rem; padding:18px 30px; background-color: var(--bg-medium);" type="button" onclick="mergeCarousel()">
|
| 1601 |
+
<i class="fas fa-object-group"></i> Объединить в группу (<span id="carousel-count">0</span>)
|
| 1602 |
</button>
|
| 1603 |
|
| 1604 |
<script>
|
|
|
|
| 1623 |
bulkFiles.forEach(item => {
|
| 1624 |
const div = document.createElement('div');
|
| 1625 |
div.id = 'preview_div_' + item.id;
|
| 1626 |
+
div.style.cssText = 'border:2px solid #e0e0e0; border-radius:20px; padding:15px; text-align:center; position:relative; width:160px; background:#fff; box-shadow:0 4px 15px rgba(0,0,0,0.05);';
|
| 1627 |
|
| 1628 |
+
let badge = item.files.length > 1 ? `<div style="position:absolute;top:-10px;right:-10px;background:var(--accent);color:var(--text-dark);border-radius:12px;padding:6px 12px;font-size:1rem;font-weight:bold;box-shadow:0 2px 8px rgba(0,0,0,0.3);"><i class="fas fa-images"></i> ${item.files.length}</div>` : '';
|
| 1629 |
|
| 1630 |
div.innerHTML = `
|
| 1631 |
+
<input type="checkbox" class="carousel-group-checkbox" value="${item.id}" style="position:absolute; top:12px; left:12px; z-index:10; width:28px; height:28px; cursor:pointer;" onchange="updateCarouselFab()">
|
| 1632 |
${badge}
|
| 1633 |
+
<img src="${item.previewUrl}" style="width:100%; height:130px; object-fit:cover; border-radius:12px; margin-bottom:15px;">
|
| 1634 |
+
<input type="number" id="price_${item.id}" placeholder="Цена" value="0" style="width:100%; padding:12px; box-sizing:border-box; margin-bottom:15px; font-size:1.15rem; border:2px solid transparent; border-radius:12px; background:#f1f3f5; outline:none; transition:0.3s;" onfocus="this.style.background='#fff';this.style.borderColor='var(--accent)';" onblur="this.style.background='#f1f3f5';this.style.borderColor='transparent';">
|
| 1635 |
+
<button type="button" class="button delete-button" style="width:100%; padding:12px; margin:0; border:none; border-radius:12px; cursor:pointer; font-size:1.05rem; min-height:auto;" onclick="removeBulk('${item.id}')"><i class="fas fa-times"></i> Убрать</button>
|
| 1636 |
`;
|
| 1637 |
container.appendChild(div);
|
| 1638 |
});
|
|
|
|
| 1705 |
}
|
| 1706 |
|
| 1707 |
async function uploadBulk() {
|
| 1708 |
+
if(bulkFiles.length === 0) return alert('Пожалуйста, сначала выберите фото!');
|
| 1709 |
const category = document.getElementById('bulk_category').value;
|
| 1710 |
const formData = new FormData();
|
| 1711 |
formData.append('action', 'bulk_add');
|
|
|
|
| 1715 |
document.getElementById('progress_container').style.display = 'block';
|
| 1716 |
const btn = document.querySelector('button[onclick="uploadBulk()"]');
|
| 1717 |
btn.disabled = true;
|
| 1718 |
+
btn.innerText = "Подготовка файлов, подождите...";
|
| 1719 |
|
| 1720 |
for(let i=0; i<bulkFiles.length; i++) {
|
| 1721 |
let item = bulkFiles[i];
|
|
|
|
| 1727 |
}
|
| 1728 |
}
|
| 1729 |
|
| 1730 |
+
btn.innerText = "Идет загрузка...";
|
| 1731 |
|
| 1732 |
const xhr = new XMLHttpRequest();
|
| 1733 |
xhr.open('POST', '{{ url_for("admin", env_id=env_id) }}', true);
|
|
|
|
| 1742 |
if (xhr.status === 200) {
|
| 1743 |
window.location.reload();
|
| 1744 |
} else {
|
| 1745 |
+
alert('Произошла ошибка при загрузке.');
|
| 1746 |
document.getElementById('progress_container').style.display = 'none';
|
| 1747 |
btn.disabled = false;
|
| 1748 |
+
btn.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Опубликовать фото на сайте';
|
| 1749 |
}
|
| 1750 |
};
|
| 1751 |
xhr.onerror = function() {
|
| 1752 |
+
alert('Сетевая ошибка. Проверьте интернет.');
|
| 1753 |
document.getElementById('progress_container').style.display = 'none';
|
| 1754 |
btn.disabled = false;
|
| 1755 |
+
btn.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Опубликовать фото на сайте';
|
| 1756 |
};
|
| 1757 |
xhr.send(formData);
|
| 1758 |
}
|
|
|
|
| 1769 |
}
|
| 1770 |
|
| 1771 |
function submitBulkDelete() {
|
| 1772 |
+
if(confirm('Вы точно уверены, что хотите удалить эти товары/фото?')) {
|
| 1773 |
const checked = document.querySelectorAll('.bulk-delete-checkbox:checked');
|
| 1774 |
const container = document.getElementById('bulk-delete-inputs');
|
| 1775 |
container.innerHTML = '';
|
|
|
|
| 1810 |
link.click();
|
| 1811 |
document.body.removeChild(link);
|
| 1812 |
})
|
| 1813 |
+
.catch(err => alert('Произошла ошибка при сохранении QR-кода'));
|
| 1814 |
}
|
| 1815 |
</script>
|
| 1816 |
</body>
|
|
|
|
| 1920 |
<style>
|
| 1921 |
:root { --bg-light: #f4f6f9; --bg-medium: #135D66; --accent: #48D1CC; --accent-hover: #77E4D8; --text-dark: #333; --text-on-accent: #003C43; --danger: #E57373; --danger-hover: #EF5350; }
|
| 1922 |
* { box-sizing: border-box; }
|
| 1923 |
+
body { font-family: 'Montserrat', sans-serif; background-color: var(--bg-light); color: var(--text-dark); padding: 20px; line-height: 1.6; margin: 0; font-size: 1.1rem; }
|
| 1924 |
.container { max-width: 900px; margin: 0 auto; background-color: #fff; padding: 30px; border-radius: 24px; box-shadow: 0 10px 40px rgba(0,0,0,0.04); }
|
| 1925 |
+
.header { padding-bottom: 20px; margin-bottom: 25px; border-bottom: 2px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; }
|
| 1926 |
+
h1, h2 { font-weight: 700; color: var(--bg-medium); margin-bottom: 15px; }
|
| 1927 |
h1 { font-size: 1.8rem; margin: 0; }
|
| 1928 |
+
.button { padding: 16px 20px; border: none; border-radius: 16px; background-color: var(--accent); color: var(--text-on-accent); font-weight: 700; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; text-decoration: none; display: inline-flex; align-items: center; gap: 10px; justify-content: center; min-height: 56px; font-size: 1.15rem; }
|
| 1929 |
.button:hover { background-color: var(--accent-hover); }
|
| 1930 |
.button:active { transform: scale(0.98); }
|
| 1931 |
+
.back-button { background-color: #6c757d; color: white; width:100%;}
|
| 1932 |
+
@media (min-width: 768px) { .back-button { width:auto; } }
|
| 1933 |
.back-button:hover { background-color: #5a6268; }
|
| 1934 |
+
.delete-button { background-color: var(--danger); color: white; padding: 12px 16px; font-size: 1.1rem; min-height: 50px; }
|
| 1935 |
.delete-button:hover { background-color: var(--danger-hover); }
|
| 1936 |
+
.order-item { background: #fff; padding: 25px; border: 2px solid #f0f0f0; border-radius: 20px; margin-bottom: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.03); transition: transform 0.2s; }
|
| 1937 |
+
.order-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; border-bottom: 2px solid #f0f0f0; padding-bottom: 15px; margin-bottom: 15px; }
|
| 1938 |
+
.order-id { font-weight: 700; color: var(--bg-medium); font-size: 1.25rem; }
|
| 1939 |
+
.order-date { color: #888; font-size: 1.05rem; margin-left: 10px;}
|
| 1940 |
+
.customer-info p { margin: 8px 0; font-size: 1.1rem; }
|
|
|
|
| 1941 |
.customer-info strong { color: var(--bg-medium); }
|
| 1942 |
+
.order-details { margin-top: 25px; background: #f8f9fa; border: 2px solid #f0f0f0; border-radius: 16px; padding: 20px; }
|
| 1943 |
+
.product-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px dashed #e0e0e0; font-size: 1.1rem; gap:15px; }
|
| 1944 |
.product-row:last-child { border-bottom: none; }
|
| 1945 |
+
.order-total { text-align: right; font-weight: 700; color: var(--bg-medium); font-size: 1.4rem; margin-top: 20px; }
|
| 1946 |
.empty-state { text-align: center; padding: 60px 20px; color: #888; }
|
| 1947 |
+
.empty-state i { font-size: 4rem; margin-bottom: 20px; color: #ddd; }
|
| 1948 |
</style>
|
| 1949 |
</head>
|
| 1950 |
<body>
|
| 1951 |
<div class="container">
|
| 1952 |
<div class="header">
|
| 1953 |
<h1><i class="fas fa-history"></i> История заказов</h1>
|
| 1954 |
+
<a href="{{ url_for('admin', env_id=env_id) }}" class="button back-button"><i class="fas fa-arrow-left"></i> Вернуться к настройкам</a>
|
| 1955 |
</div>
|
| 1956 |
{% if orders %}
|
| 1957 |
{% for order in orders %}
|
|
|
|
| 1961 |
<span class="order-id">Заказ #{{ order['id'] }}</span>
|
| 1962 |
<span class="order-date">{{ order['date'] }}</span>
|
| 1963 |
</div>
|
| 1964 |
+
<div style="display: flex; gap: 10px; align-items: center; width:100%; justify-content:space-between; margin-top:10px;">
|
| 1965 |
+
<a href="{{ url_for('order_invoice', env_id=env_id, order_id=order['id']) }}" target="_blank" class="button" style="padding: 12px 20px; font-size: 1.1rem; min-height: 50px;"><i class="fas fa-file-invoice"></i> Накладная</a>
|
| 1966 |
<form method="POST" action="{{ url_for('delete_order', env_id=env_id, order_id=order['id']) }}" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить этот заказ из истории?');">
|
| 1967 |
<button type="submit" class="button delete-button"><i class="fas fa-trash-alt"></i></button>
|
| 1968 |
</form>
|
|
|
|
| 1970 |
</div>
|
| 1971 |
<div class="customer-info">
|
| 1972 |
<p><strong>Покупатель:</strong> {{ order['customer_name'] }}</p>
|
| 1973 |
+
<p><strong>Телефон:</strong> <a href="tel:{{ order['customer_phone'] }}" style="color:var(--accent); font-weight:700; text-decoration:none;">{{ order['customer_phone'] }}</a></p>
|
| 1974 |
</div>
|
| 1975 |
<div class="order-details">
|
| 1976 |
{% for pid, item in order['items'].items() %}
|
| 1977 |
<div class="product-row">
|
| 1978 |
{% if item['photo'] %}
|
| 1979 |
+
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ item['photo'] }}" style="width:75px; height:75px; object-fit:cover; border-radius:12px;">
|
| 1980 |
{% else %}
|
| 1981 |
+
<div style="width:75px; height:75px; background:#e0e0e0; display:flex; align-items:center; justify-content:center; border-radius:12px; font-size:1rem; color:#888;">Фото</div>
|
| 1982 |
{% endif %}
|
| 1983 |
+
<span style="flex-grow:1; font-weight:700;">{{ item['qty'] }} шт.</span>
|
| 1984 |
<span style="font-weight:700;">{{ "%.2f"|format(item['price']|float * item['qty']|int) }} {{ currency }}</span>
|
| 1985 |
</div>
|
| 1986 |
{% endfor %}
|
|
|
|
| 1993 |
{% else %}
|
| 1994 |
<div class="empty-state">
|
| 1995 |
<i class="fas fa-shopping-cart"></i>
|
| 1996 |
+
<p style="font-size: 1.25rem;">История заказов пуста.</p>
|
| 1997 |
</div>
|
| 1998 |
{% endif %}
|
| 1999 |
</div>
|
|
|
|
| 2321 |
|
| 2322 |
data['products'] = new_products
|
| 2323 |
save_env_data(env_id, data)
|
| 2324 |
+
flash(f"Успешно удалено.", 'success')
|
| 2325 |
|
| 2326 |
elif action == 'add_block':
|
| 2327 |
b_type = request.form.get('block_type')
|
|
|
|
| 2365 |
})
|
| 2366 |
data['blocks'] = blocks
|
| 2367 |
save_env_data(env_id, data)
|
| 2368 |
+
flash("Кнопка добавлена.", "success")
|
| 2369 |
|
| 2370 |
elif action == 'edit_block':
|
| 2371 |
b_id = request.form.get('block_id')
|
|
|
|
| 2420 |
break
|
| 2421 |
data['blocks'] = blocks
|
| 2422 |
save_env_data(env_id, data)
|
| 2423 |
+
flash("Кнопка сохранена.", "success")
|
| 2424 |
|
| 2425 |
elif action == 'delete_block':
|
| 2426 |
b_id = request.form.get('block_id')
|
|
|
|
| 2433 |
blocks = [b for b in blocks if b.get('id') != b_id]
|
| 2434 |
data['blocks'] = blocks
|
| 2435 |
save_env_data(env_id, data)
|
| 2436 |
+
flash("Кнопка успешно удалена.", "success")
|
| 2437 |
|
| 2438 |
elif action == 'move_block_up':
|
| 2439 |
b_id = request.form.get('block_id')
|
|
|
|
| 2442 |
blocks[idx], blocks[idx-1] = blocks[idx-1], blocks[idx]
|
| 2443 |
data['blocks'] = blocks
|
| 2444 |
save_env_data(env_id, data)
|
| 2445 |
+
flash("Порядок изменён.", "success")
|
| 2446 |
|
| 2447 |
elif action == 'move_block_down':
|
| 2448 |
b_id = request.form.get('block_id')
|
|
|
|
| 2451 |
blocks[idx], blocks[idx+1] = blocks[idx+1], blocks[idx]
|
| 2452 |
data['blocks'] = blocks
|
| 2453 |
save_env_data(env_id, data)
|
| 2454 |
+
flash("Порядок изменён.", "success")
|
| 2455 |
|
| 2456 |
elif action == 'add_category':
|
| 2457 |
category_name = request.form.get('category_name', '').strip()
|
|
|
|
| 2459 |
categories.append(category_name)
|
| 2460 |
data['categories'] = categories
|
| 2461 |
save_env_data(env_id, data)
|
| 2462 |
+
flash(f"Папка успешно создана.", 'success')
|
| 2463 |
elif not category_name:
|
| 2464 |
+
flash("Название папки не может быть пустым.", 'error')
|
| 2465 |
else:
|
| 2466 |
+
flash(f"Такая папка уже существует.", 'error')
|
| 2467 |
|
| 2468 |
elif action == 'delete_category':
|
| 2469 |
category_to_delete = request.form.get('category_name')
|
|
|
|
| 2475 |
data['categories'] = categories
|
| 2476 |
data['products'] = products
|
| 2477 |
save_env_data(env_id, data)
|
| 2478 |
+
flash(f"Папка удалена.", 'success')
|
| 2479 |
|
| 2480 |
elif action == 'update_settings':
|
| 2481 |
settings['admin_password_enabled'] = 'admin_password_enabled' in request.form
|
|
|
|
| 2514 |
except Exception: pass
|
| 2515 |
data['settings'] = settings
|
| 2516 |
save_env_data(env_id, data)
|
| 2517 |
+
flash("Настройки успешно сохранены.", 'success')
|
| 2518 |
|
| 2519 |
elif action == 'edit_product':
|
| 2520 |
product_id = request.form.get('product_id')
|
|
|
|
| 2555 |
|
| 2556 |
data['products'] = products
|
| 2557 |
save_env_data(env_id, data)
|
| 2558 |
+
flash("Товар изменён.", 'success')
|
| 2559 |
|
| 2560 |
elif action == 'delete_product':
|
| 2561 |
product_id = request.form.get('product_id')
|
|
|
|
| 2570 |
except Exception: pass
|
| 2571 |
data['products'] = products
|
| 2572 |
save_env_data(env_id, data)
|
| 2573 |
+
flash("Товар удалён.", 'success')
|
| 2574 |
|
| 2575 |
return redirect(url_for('admin', env_id=env_id, p=page))
|
| 2576 |
except Exception as e:
|
| 2577 |
+
flash(f"Произошла ошибка: {e}", 'error')
|
| 2578 |
return redirect(url_for('admin', env_id=env_id, p=page))
|
| 2579 |
|
| 2580 |
filtered_products = list(reversed(products))
|
|
|
|
| 2631 |
orders = data.get('orders', [])
|
| 2632 |
data['orders'] = [o for o in orders if o.get('id') != order_id]
|
| 2633 |
save_env_data(env_id, data)
|
| 2634 |
+
flash("Заказ успешно удален из истории.", "success")
|
| 2635 |
return redirect(url_for('admin_orders', env_id=env_id))
|
| 2636 |
|
| 2637 |
if __name__ == '__main__':
|