Kgshop commited on
Commit
a26de27
·
verified ·
1 Parent(s): b4abb76

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +317 -224
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: 70px; height: 70px; border-radius: 12px; object-fit: cover; }
551
- .cart-item-info { flex-grow: 1; }
552
- .cart-item-price { font-size: 1.05rem; font-weight: bold; color: var(--accent); margin-bottom: 5px; }
553
- .cart-controls { display: flex; justify-content: space-between; align-items: center; background: var(--bg-medium); border-radius: 12px; overflow: hidden; height: 40px;}
554
- .cart-controls button { background: var(--accent); color: var(--text-dark); border: none; width: 40px; height: 100%; font-weight: bold; cursor: pointer; font-size: 1.3rem; }
555
- .cart-controls input { width: 46px; text-align: center; border: none; background: transparent; color: white; font-weight: bold; font-size: 1.1rem; outline: none; -moz-appearance: textfield; }
 
 
 
 
 
 
 
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
- <h2 style="margin-top: 20px; margin-bottom: 25px; color: var(--accent); text-align: center;">Корзина</h2>
 
 
 
 
 
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
- <div style="margin-top: 25px; background: var(--card-bg); padding: 20px; border-radius: 20px; border: 1px solid rgba(255,255,255,0.1);">
664
- <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;">
665
- <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;">
 
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">В этой категории нет фото.</div>';
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/70x70.png?text=Фото`;
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
- <div class="cart-controls" style="margin-top:0;">
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;">Корзина пуста</p>';
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@300;400;500;600;700&display=swap" rel="stylesheet">
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
- .container { max-width: 1200px; margin: 0 auto; background-color: #fff; padding: 30px; border-radius: 24px; box-shadow: 0 10px 40px rgba(0,0,0,0.04); }
1037
- .header { padding-bottom: 20px; margin-bottom: 30px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;}
1038
- .header .logo-title-container img { height: 56px; width: 56px; border-radius: 50%; object-fit: cover; border: 2px solid var(--bg-medium);}
1039
- h1, h2, h3 { font-weight: 600; color: var(--bg-medium); margin-bottom: 15px; }
1040
- h1 { font-size: 1.8rem; }
 
 
 
 
 
 
 
 
 
 
 
 
1041
  h2 { font-size: 1.5rem; margin-top: 20px; display: flex; align-items: center; gap: 10px; }
1042
- h3 { font-size: 1.25rem; color: #004D40; margin-top: 20px; }
1043
- .section { margin-bottom: 35px; padding: 25px; background-color: #fff; border: 1px solid #f0f0f0; border-radius: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.02); }
1044
- form { margin-bottom: 20px; }
1045
- label { font-weight: 600; margin-top: 12px; display: block; color: #555; font-size: 0.95rem;}
1046
- input[type="text"], input[type="number"], input[type="password"], input[type="tel"], textarea, select { width: 100%; padding: 14px 16px; margin-top: 8px; border: 2px solid transparent; background: #f8f9fa; border-radius: 14px; font-size: 1rem; box-sizing: border-box; transition: all 0.3s ease; min-height: 52px;}
1047
- input:focus, textarea:focus, select:focus { border-color: var(--accent); background: #fff; outline: none; box-shadow: 0 0 0 4px rgba(72, 209, 204, 0.15); }
1048
- textarea { min-height: 100px; resize: vertical; }
1049
- input[type="file"] { padding: 10px; background-color: #f8f9fa; cursor: pointer; border: 1px dashed #ccc; border-radius: 14px;}
1050
- input[type="file"]::file-selector-button { padding: 10px 16px; border-radius: 10px; background-color: #e9ecef; border: none; cursor: pointer; margin-right: 12px; font-weight: 600; transition: background 0.2s;}
1051
- input[type="file"]::file-selector-button:hover { background-color: #dee2e6; }
1052
- input[type="checkbox"] { margin-right: 10px; vertical-align: middle; width: 22px; height: 22px; cursor: pointer; accent-color: var(--bg-medium);}
1053
- button, .button { padding: 12px 24px; border: none; border-radius: 14px; background-color: var(--accent); color: var(--text-on-accent); font-weight: 700; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; margin-top: 15px; font-size: 1.05rem; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; line-height: 1.2; min-height: 52px; justify-content: center;}
 
 
 
 
 
 
 
 
 
 
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: 20px; border-radius: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.03); border: 1px solid #f0f0f0; transition: transform 0.2s; }
1062
- .item:hover { transform: translateY(-3px); box-shadow: 0 8px 25px rgba(0,0,0,0.06); }
1063
- .item p { margin: 6px 0; font-size: 0.95rem; color: #555; }
1064
  .item strong { color: var(--text-dark); }
1065
- .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
1066
- .edit-form-container { margin-top: 20px; padding: 20px; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 16px; display: none; }
1067
- details { background-color: #fff; border: 1px solid #f0f0f0; border-radius: 16px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.02); }
1068
- details > summary { cursor: pointer; font-weight: 600; color: var(--bg-medium); display: block; padding: 20px 24px; list-style: none; position: relative; font-size: 1.15rem; }
1069
- details > summary:hover { background-color: #f8f9fa; border-radius: 16px;}
 
 
 
 
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: 1px solid #f0f0f0; border-radius: 16px 16px 0 0; }
1073
- details .form-content { padding: 24px; }
1074
- .photo-preview { display: flex; gap: 8px; flex-wrap: wrap; }
1075
- .photo-preview img { width: 90px; height: 90px; border-radius: 12px; border: 1px solid #e0e0e0; object-fit: cover;}
1076
- .message { padding: 16px 20px; border-radius: 14px; margin-bottom: 20px; font-size: 1rem; font-weight: 500;}
1077
- .message.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb;}
1078
- .message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;}
1079
- .current-avatar { max-width: 64px; max-height: 64px; border-radius: 50%; vertical-align: middle; margin-left: 12px; border: 2px solid var(--bg-medium);}
1080
- .block-item { display: flex; justify-content: space-between; align-items: center; background: #fff; padding: 18px 24px; border: 1px solid #f0f0f0; border-radius: 16px; margin-bottom: 12px; flex-wrap: wrap; gap: 15px; box-shadow: 0 2px 10px rgba(0,0,0,0.02);}
1081
- .block-controls { display: flex; gap: 8px; }
1082
- .btn-small { padding: 10px 14px; font-size: 0.95rem; min-height: auto;}
1083
- .pagination { display: flex; justify-content: center; gap: 10px; margin-top: 35px; flex-wrap: wrap; align-items: center; }
1084
- .pagination .button { min-width: 48px; text-align: center; padding: 12px 16px; margin: 0; border-radius: 12px; }
 
 
 
 
 
 
 
 
 
 
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: 64px; height: 64px; border: 6px solid rgba(255,255,255,0.2);
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;">Обработка данных...</h2>
1105
  </div>
1106
  <div class="container">
1107
  <div class="header">
1108
- <div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
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 style="display: flex; gap: 10px; flex-wrap: wrap;">
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: 2px 8px; font-size: 0.85rem; font-weight: bold; margin-left: 8px; border: 2px solid #E57373;">{{ new_orders_count }}</span>
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> Выход</a>
1123
  {% endif %}
1124
  </div>
1125
  </div>
@@ -1150,80 +1238,80 @@ ADMIN_TEMPLATE = '''
1150
  </div>
1151
  </div>
1152
 
1153
- <label>Должность / Специальность:</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>Коротко о себе / Статус:</label>
1160
  <textarea name="about_text" rows="3">{{ settings.about_text }}</textarea>
1161
 
1162
- <label>Аватар ото профиля):</label>
1163
  <input type="file" name="chat_avatar" accept="image/*">
1164
  {% if settings.chat_avatar %}
1165
- <p style="font-size: 0.95rem; margin-top: 10px;">Текущий аватар: <img src="{{ chat_avatar_url }}" class="current-avatar"></p>
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>Валюта (для товаров/услуг):</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: 30px; font-size:1.15rem;"><i class="fas fa-shopping-cart"></i> Настройки заказов</h4>
1183
- <label style="display: flex; align-items: center; gap: 8px;">
1184
  <input type="checkbox" name="enable_cart" {% if settings.enable_cart %}checked{% endif %}>
1185
- Включить возможность заказа
1186
  </label>
1187
- <label>Мессенджер для приема заказов:</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>Номер (WhatsApp) или Username без @ (Telegram):</label>
1194
- <input type="text" name="order_contact" value="{{ settings.order_contact }}" placeholder="Например: 996555123456 или myusername">
1195
 
1196
- <div style="background: #f1f3f5; padding: 24px; border-radius: 16px; margin-top: 30px; border:1px solid #e9ecef;">
1197
- <h4 style="margin-top: 0; color: var(--bg-medium); font-size: 1.15rem;"><i class="fas fa-lock"></i> Пароль для входа сюда</h4>
1198
- <label style="display: flex; align-items: center; gap: 8px;"><input type="checkbox" name="admin_password_enabled" {% if settings.admin_password_enabled %}checked{% endif %}> Требовать пароль</label>
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: 25px;"><i class="fas fa-save"></i> Сохранить настройки</button>
1204
  </form>
1205
  </div>
1206
  </details>
1207
  </div>
1208
 
1209
  <div class="section">
1210
- <h2><i class="fas fa-link"></i> Ссылки и Блоки</h2>
1211
  <details>
1212
- <summary><i class="fas fa-plus-circle"></i> Добавить блок</summary>
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>Тип блока:</label>
1217
  <select name="block_type" id="add_block_type" onchange="toggleBlockFields('add_')">
1218
- <option value="link">Кнопка-ссылка</option>
1219
- <option value="text">Текстовый блок</option>
1220
- <option value="pdf">PDF Каталог</option>
1221
  </select>
1222
- <label>Заголовок / Текст кнопки:</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>URL / Номер / Email (Вставьте ссылку, или телефон начиная с +):</label>
1237
- <input type="text" name="block_url" placeholder="https://... или +996... или mail@.com">
1238
  </div>
1239
  <div id="add_block_pdf_div" style="display: none;">
1240
- <label>Загрузить PDF:</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>Текст блока:</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: 15px;">
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.15rem;">
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: #888; font-size: 0.95rem; margin-left: 10px;">
1264
  ({% if block.type == 'link' %}Ссылка{% elif block.type == 'pdf' %}PDF{% else %}Текст{% endif %})
1265
  </span>
1266
- {% if block.type == 'link' %}<br><small style="font-size: 0.95rem; color: #666; margin-top: 5px; display: block;"><a href="{{ block.url }}" target="_blank" rel="noopener noreferrer">{{ block.url }}</a></small>{% endif %}
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('Удалить блок?')) return false; showLoadingOverlay(); return true;">
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>Тип блока:</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 %}>Кнопка-ссылка</option>
1296
- <option value="text" {% if block.type == 'text' %}selected{% endif %}>Текстовый блок</option>
1297
- <option value="pdf" {% if block.type == 'pdf' %}selected{% endif %}>PDF Каталог</option>
1298
  </select>
1299
- <label>Заголовок / Текст кнопки:</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>URL / Номер / Email (Вставьте ссылку, или телефон начиная с +):</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 (заменит старый):</label>
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>Текст блока:</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.05rem; color:#888;">Блоки не добавлены.</p>
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-tags"></i> Категории товаров/фото</h2>
1356
  <details>
1357
- <summary><i class="fas fa-plus-circle"></i> Добавить новую категорию</summary>
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>Название новой категории:</label>
1362
  <input type="text" name="category_name" required>
1363
- <button type="submit" class="add-button"><i class="fas fa-plus"></i> Добавить</button>
1364
  </form>
1365
  </div>
1366
  </details>
1367
  {% if categories %}
1368
- <div class="item-list" style="margin-top:20px;">
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.1rem; font-weight: 600;">{{ category }}</span>
1372
- <form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" style="margin: 0;" onsubmit="if(!confirm('Вы уверены? Товары этой категории будут помечены как \\'Без категории\\'.')) return false;">
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.05rem; margin-top:15px; color:#888;">Категорий пока нет.</p>
1382
  {% endif %}
1383
  </div>
1384
 
1385
  <div class="section">
1386
- <h2><i class="fas fa-box-open"></i> Фото / Товары</h2>
1387
  <details open>
1388
- <summary><i class="fas fa-images"></i> Массовая загрузка товаров ото)</summary>
1389
  <div class="form-content">
1390
- <label>Категория для новых фото:</label>
1391
  <select id="bulk_category">
1392
- <option value="Без категории">Без категории</option>
1393
  {% for category in categories %}
1394
  <option value="{{ category }}">{{ category }}</option>
1395
  {% endfor %}
1396
  </select>
1397
 
1398
- <label style="margin-top: 15px;">Выберите фото (можно много без ограничений):</label>
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: 20px;"></div>
1402
 
1403
- <div id="progress_container" style="display:none; margin-top:25px;">
1404
- <div style="width:100%; background:#eee; border-radius:12px; overflow:hidden;">
1405
- <div id="progress_bar" style="width:0%; height:24px; background:var(--accent); transition:width 0.2s;"></div>
1406
  </div>
1407
- <p id="progress_text" style="text-align:center; font-weight:bold; margin-top:8px;">0%</p>
1408
  </div>
1409
 
1410
- <button type="button" class="add-button" style="margin-top: 25px; width:100%; font-size:1.1rem; min-height:56px;" onclick="uploadBulk()"><i class="fas fa-upload"></i> Загрузить на сайт</button>
1411
  </div>
1412
  </details>
1413
 
1414
- <h3 style="margin-top: 40px; margin-bottom: 20px;">Список фото / товаров:</h3>
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
- <input type="checkbox" class="bulk-delete-checkbox" value="{{ product.product_id }}" onchange="updateBulkDeleteFab()" style="margin-top: 35px;">
1421
- <div class="photo-preview" style="flex-shrink: 0; display:flex; flex-wrap:wrap; max-width:200px;">
 
 
 
 
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/90x90.png?text=Нет+фото" alt="Нет фото">
1428
  {% endif %}
1429
  </div>
1430
- <div style="flex-grow: 1;">
1431
- <p style="font-size: 1.05rem;"><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1432
- <p style="font-size: 1.05rem;"><strong>Цена:</strong> {% if product.get('price', 0) > 0 %}{{ "%.2f"|format(product.get('price', 0)) }} {{ currency_code }}{% else %}0{% endif %}</p>
 
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> Редактировать</button>
1438
- <form method="POST" action="{{ url_for('admin', env_id=env_id, p=page) }}" style="margin:0;" onsubmit="if(!confirm('Удалить товар?')) return false; showLoadingOverlay(); return true;">
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.15rem;"><i class="fas fa-edit"></i> Редактирование товара</h4>
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>Цена:</label>
1452
  <input type="number" name="price" step="0.01" value="{{ product.get('price', 0) }}">
1453
 
1454
- <label>Категория:</label>
1455
  <select name="category">
1456
- <option value="Без категории">Без категории</option>
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>Заменить фотографии (удалит старые, можно выбрать несколько):</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">&laquo;</a>
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: 10px; color: var(--bg-medium); font-weight: bold;">...</span>
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">&raquo;</a>
1488
  {% endif %}
1489
  </div>
1490
  {% endif %}
1491
 
1492
  {% else %}
1493
- <p style="font-size: 1.15rem; text-align: center; padding: 50px; color:#888;">Товаров пока нет.</p>
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:30px; box-shadow:0 6px 20px rgba(0,0,0,0.3); font-size:1.1rem; padding:16px 28px;" type="button" onclick="submitBulkDelete()">
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:30px; box-shadow:0 6px 20px rgba(0,0,0,0.3); font-size:1.1rem; padding:16px 28px; background-color: var(--bg-medium);" type="button" onclick="mergeCarousel()">
1508
- <i class="fas fa-object-group"></i> В карусель (<span id="carousel-count">0</span>)
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:1px solid #e0e0e0; border-radius:16px; padding:12px; text-align:center; position:relative; width:150px; background:#fff; box-shadow:0 4px 15px rgba(0,0,0,0.02);';
1534
 
1535
- let badge = item.files.length > 1 ? `<div style="position:absolute;top:-8px;right:-8px;background:var(--accent);color:var(--text-dark);border-radius:12px;padding:4px 8px;font-size:0.8rem;font-weight:bold;box-shadow:0 2px 8px rgba(0,0,0,0.2);"><i class="fas fa-images"></i> ${item.files.length}</div>` : '';
1536
 
1537
  div.innerHTML = `
1538
- <input type="checkbox" class="carousel-group-checkbox" value="${item.id}" style="position:absolute; top:8px; left:8px; z-index:10; width:22px; height:22px; cursor:pointer;" onchange="updateCarouselFab()">
1539
  ${badge}
1540
- <img src="${item.previewUrl}" style="width:100%; height:120px; object-fit:cover; border-radius:10px; margin-bottom:12px;">
1541
- <input type="number" id="price_${item.id}" placeholder="Цена" value="0" style="width:100%; padding:10px; box-sizing:border-box; margin-bottom:12px; font-size:1rem; border:2px solid transparent; border-radius:10px; 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';">
1542
- <button type="button" class="button delete-button" style="width:100%; padding:10px; margin:0; border:none; border-radius:10px; cursor:pointer; font-size:0.95rem; min-height:auto;" onclick="removeBulk('${item.id}')"><i class="fas fa-trash"></i> Удалить</button>
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('Ошибка при скачивании QR-кода'));
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: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; }
1833
- h1, h2 { font-weight: 600; color: var(--bg-medium); margin-bottom: 15px; }
1834
  h1 { font-size: 1.8rem; margin: 0; }
1835
- .button { padding: 12px 20px; border: none; border-radius: 14px; 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: 8px; justify-content: center; min-height: 48px; }
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: 10px 14px; font-size: 0.95rem; min-height: 42px; }
1841
  .delete-button:hover { background-color: var(--danger-hover); }
1842
- .order-item { background: #fff; padding: 25px; border: 1px solid #f0f0f0; border-radius: 16px; margin-bottom: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.03); transition: transform 0.2s; }
1843
- .order-item:hover { transform: translateY(-3px); box-shadow: 0 8px 25px rgba(0,0,0,0.06); }
1844
- .order-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; border-bottom: 1px solid #f0f0f0; padding-bottom: 15px; margin-bottom: 15px; }
1845
- .order-id { font-weight: 700; color: var(--bg-medium); font-size: 1.15rem; }
1846
- .order-date { color: #888; font-size: 0.95rem; margin-left: 10px;}
1847
- .customer-info p { margin: 5px 0; font-size: 1rem; }
1848
  .customer-info strong { color: var(--bg-medium); }
1849
- .order-details { margin-top: 20px; background: #f8f9fa; border: 1px solid #f0f0f0; border-radius: 14px; padding: 20px; }
1850
- .product-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px dashed #e0e0e0; font-size: 1.05rem; gap:15px; }
1851
  .product-row:last-child { border-bottom: none; }
1852
- .order-total { text-align: right; font-weight: 700; color: var(--bg-medium); font-size: 1.3rem; margin-top: 20px; }
1853
  .empty-state { text-align: center; padding: 60px 20px; color: #888; }
1854
- .empty-state i { font-size: 3.5rem; margin-bottom: 20px; color: #ddd; }
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> Назад</a>
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: 10px 16px; font-size: 0.95rem; min-height: 42px;"><i class="fas fa-file-invoice"></i> Накладная</a>
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:600; text-decoration:none;">{{ order['customer_phone'] }}</a></p>
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:65px; height:65px; object-fit:cover; border-radius:10px;">
1887
  {% else %}
1888
- <div style="width:65px; height:65px; background:#e0e0e0; display:flex; align-items:center; justify-content:center; border-radius:10px; font-size:0.85rem; color:#888;">Фото</div>
1889
  {% endif %}
1890
- <span style="flex-grow:1; font-weight:600;">{{ item['qty'] }} шт.</span>
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.1rem;">История заказов пуста.</p>
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"Удалено {len(product_ids)} товаров.", 'success')
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("Блок добавлен.", "success")
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("Блок обновлен.", "success")
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("Блок удален.", "success")
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("Блок перемещен.", "success")
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("Блок перемещен.", "success")
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"Категория добавлена.", 'success')
2370
  elif not category_name:
2371
- flash("Название категории пустое.", 'error')
2372
  else:
2373
- flash(f"Категория уже существует.", 'error')
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"Категория удалена.", 'success')
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("Настройки обновлены.", 'success')
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("Обновлено.", 'success')
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("Удалено.", 'success')
2481
 
2482
  return redirect(url_for('admin', env_id=env_id, p=page))
2483
  except Exception as e:
2484
- flash(f"Ошибка: {e}", 'error')
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__':