Kgshop commited on
Commit
7b17a4d
·
verified ·
1 Parent(s): 0809651

Update app.py

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