Kgshop commited on
Commit
a30c32f
·
verified ·
1 Parent(s): be587ad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +173 -443
app.py CHANGED
@@ -41,15 +41,14 @@ CURRENCY_NAME = 'Кыргызский сом'
41
  DOWNLOAD_RETRIES = 3
42
  DOWNLOAD_DELAY = 5
43
 
 
 
44
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
45
  if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
46
- return False
47
-
48
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
49
-
50
  files_to_download = [specific_file] if specific_file else SYNC_FILES
51
  all_successful = True
52
-
53
  for file_name in files_to_download:
54
  success = False
55
  for attempt in range(retries + 1):
@@ -67,7 +66,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
67
  success = True
68
  break
69
  except RepositoryNotFoundError:
70
- return False
71
  except HfHubHTTPError as e:
72
  if e.response.status_code == 404:
73
  if attempt == 0 and not os.path.exists(file_name):
@@ -82,16 +81,13 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
82
  else:
83
  pass
84
  except requests.exceptions.RequestException as e:
85
- pass
86
  except Exception as e:
87
- pass
88
-
89
  if attempt < retries:
90
  time.sleep(delay)
91
-
92
  if not success:
93
  all_successful = False
94
-
95
  return all_successful
96
 
97
  def upload_db_to_hf(specific_file=None):
@@ -100,7 +96,6 @@ def upload_db_to_hf(specific_file=None):
100
  try:
101
  api = HfApi()
102
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
103
-
104
  for file_name in files_to_upload:
105
  if os.path.exists(file_name):
106
  try:
@@ -113,7 +108,7 @@ def upload_db_to_hf(specific_file=None):
113
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
114
  )
115
  except Exception as e:
116
- pass
117
  else:
118
  pass
119
  except Exception as e:
@@ -132,28 +127,33 @@ def load_data():
132
  "returns": "Возврат и обмен товара возможен в течение 14 дней с момента покупки, при условии сохранения товарного вида, упаковки и чека. Некоторые категории товаров могут иметь особые условия возврата. Пожалуйста, свяжитесь с нами для оформления возврата или обмена.",
133
  "contact": f"Наш магазин находится по адресу: {STORE_ADDRESS}. Связаться с нами можно по телефону: {WHATSAPP_NUMBER} или через WhatsApp по этому же номеру. Мы работаем ежедневно с 9:00 до 18:00."
134
  }
135
- default_data = {'products': [], 'categories': [], 'orders': {}, 'organization_info': default_organization_info}
 
 
136
  data = default_data
137
  try:
138
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
139
  data = json.load(file)
140
  if not isinstance(data, dict):
141
- raise FileNotFoundError
142
  if 'products' not in data: data['products'] = []
143
  if 'categories' not in data: data['categories'] = []
144
  if 'orders' not in data: data['orders'] = {}
145
  if 'organization_info' not in data: data['organization_info'] = default_organization_info
 
 
146
  except FileNotFoundError:
147
  if download_db_from_hf(specific_file=DATA_FILE):
148
  try:
149
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
150
  data = json.load(file)
151
- if not isinstance(data, dict):
152
- data = default_data
153
  if 'products' not in data: data['products'] = []
154
  if 'categories' not in data: data['categories'] = []
155
  if 'orders' not in data: data['orders'] = {}
156
  if 'organization_info' not in data: data['organization_info'] = default_organization_info
 
 
157
  except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
158
  data = default_data
159
  else:
@@ -163,12 +163,13 @@ def load_data():
163
  try:
164
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
165
  data = json.load(file)
166
- if not isinstance(data, dict):
167
- data = default_data
168
  if 'products' not in data: data['products'] = []
169
  if 'categories' not in data: data['categories'] = []
170
  if 'orders' not in data: data['orders'] = {}
171
  if 'organization_info' not in data: data['organization_info'] = default_organization_info
 
 
172
  except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
173
  data = default_data
174
  else:
@@ -198,6 +199,7 @@ def save_data(data):
198
  if 'categories' not in data: data['categories'] = []
199
  if 'orders' not in data: data['orders'] = {}
200
  if 'organization_info' not in data: data['organization_info'] = {}
 
201
 
202
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
203
  json.dump(data, file, ensure_ascii=False, indent=4)
@@ -220,7 +222,7 @@ def generate_ai_description_from_image(image_data, language):
220
 
221
  try:
222
  if not image_data:
223
- raise ValueError("Файл изображения не найден.")
224
  image_stream = io.BytesIO(image_data)
225
  image = Image.open(image_stream).convert('RGB')
226
  except Exception as e:
@@ -241,35 +243,32 @@ def generate_ai_description_from_image(image_data, language):
241
  final_prompt = f"{base_prompt}{lang_suffix}"
242
 
243
  try:
244
- model = genai.GenerativeModel('learnlm-2.0-flash-experimental')
245
-
246
  response = model.generate_content([final_prompt, image])
247
-
248
  if hasattr(response, 'text'):
249
  return response.text
250
  else:
251
  if response.parts:
252
- return "".join(part.text for part in response.parts if hasattr(part, 'text'))
253
  else:
254
  response.resolve()
255
  return response.text
256
-
257
  except Exception as e:
258
  if "API key not valid" in str(e):
259
- raise ValueError("Внутренняя ошибка конфигурации API.")
260
  elif " Billing account not found" in str(e):
261
- raise ValueError("Проблема с биллингом аккаунта Google Cloud. Проверьте ваш аккаунт.")
262
  elif "Could not find model" in str(e):
263
- raise ValueError(f"Модель 'learnlm-2.0-flash-experimental' не найдена или недоступна.")
264
  elif "resource has been exhausted" in str(e).lower():
265
- raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
266
  elif "content has been blocked" in str(e).lower():
267
- reason = "неизвестна"
268
- if hasattr(e, 'response') and hasattr(e.response, 'prompt_feedback') and e.response.prompt_feedback.block_reason:
269
  reason = e.response.prompt_feedback.block_reason
270
- raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другое изображение или запрос.")
271
  else:
272
- raise ValueError(f"Ошибка при генерации контента: {e}")
273
 
274
  def generate_chat_response(message, chat_history_from_client):
275
  if not configure_gemini():
@@ -301,7 +300,6 @@ def generate_chat_response(message, chat_history_from_client):
301
  if organization_info.get("contact"):
302
  org_info_str += f"Контактная информация: {organization_info['contact']}\n"
303
 
304
-
305
  system_instruction_content = (
306
  "Ты - доброжелательный и очень полезный виртуальный консультант для магазина O&CO. "
307
  "Твоя задача - помогать пользователям находить товары, отвечать на вопросы о них, предлагать варианты, а также предоставлять информацию о магазине. "
@@ -323,7 +321,7 @@ def generate_chat_response(message, chat_history_from_client):
323
  response = None
324
 
325
  try:
326
- model = genai.GenerativeModel('learnlm-2.0-flash-experimental', system_instruction=system_instruction_content)
327
 
328
  model_chat_history_for_gemini = []
329
  for entry in chat_history_from_client:
@@ -334,7 +332,6 @@ def generate_chat_response(message, chat_history_from_client):
334
  })
335
 
336
  chat = model.start_chat(history=model_chat_history_for_gemini)
337
-
338
  response = chat.send_message(message, generation_config={'max_output_tokens': 1000})
339
 
340
  if hasattr(response, 'text'):
@@ -347,82 +344,46 @@ def generate_chat_response(message, chat_history_from_client):
347
  generated_text = response.text
348
  else:
349
  raise ValueError("AI did not return a valid text response.")
350
-
351
  return generated_text
352
-
353
  except Exception as e:
354
  if "API key not valid" in str(e):
355
- return "Внутренняя ошибка конфигурации API."
356
  elif " Billing account not found" in str(e):
357
- return "Проблема с биллингом аккаунта Google Cloud."
358
  elif "Could not find model" in str(e):
359
- return "Модель 'learnlm-2.0-flash-experimental' не найдена или недоступна."
360
  elif "resource has been exhausted" in str(e).lower():
361
- return "Квота запросов исчерпана. Попробуйте позже."
362
  elif "content has been blocked" in str(e).lower() or (response is not None and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason):
363
- reason = response.prompt_feedback.block_reason if (response and hasattr(response, 'prompt_feedback')) else "неизвестна"
364
- return f"Извините, Ваш запрос был заблокирован из-за политики безопасности (причина: {reason}). Пожалуйста, переформулируйте его."
365
  else:
366
- return f"Извините, произошла ошибка: {e}"
367
-
368
- def generate_order_whatsapp_message_ai(order):
369
- if not configure_gemini():
370
- raise ValueError("Google AI API не настроен.")
371
-
372
- customer_name = order.get('user_info', {}).get('name', 'клиент')
373
- order_id = order.get('id', 'N/A')
374
- total_price = f"{order.get('total_price', 0):.2f} {CURRENCY_CODE}"
375
- product_summaries = []
376
- for item in order.get('cart', []):
377
- product_summaries.append(f"{item.get('name')} x{item.get('quantity')} ({item.get('price'):.2f} {CURRENCY_CODE} каждый)")
378
-
379
- products_list_str = ", ".join(product_summaries) if product_summaries else "товары"
380
-
381
- prompt = (
382
- f"Ты - виртуальный помощник для магазина O&CO. Твоя задача - составить вежливое и информативное сообщение "
383
- f"для клиента магазина по поводу его заказа. Используй информацию о заказе, но избегай уп��минания "
384
- f"личной информации клиента (например, его номер телефона или адрес). "
385
- f"Сообщение должно быть на русском языке. "
386
- f"Предложи клиенту связаться для подтверждения и уточнения деталей доставки и оплаты."
387
- f"Начни с обращения 'Добрый день, [Имя клиента]!' или 'Добрый день!' если имя не указано."
388
- f"Примерный формат сообщения: 'Добрый день, [Имя клиента]! Ваш заказ №[ID заказа] на сумму [Сумма] {CURRENCY_CODE} с товарами [Список товаров] успешно создан. Пожалуйста, свяжитесь с нами для подтверждения и уточнения деталей доставки и оплаты. С уважением, команда O&CO.'. "
389
- f"Сделай его чуть более развернутым, дружелюбным и профессиональным, но при этом лаконичным.\n\n"
390
- f"Информация о заказе:\n"
391
- f"Имя клиента: {customer_name}\n"
392
- f"ID заказа: {order_id}\n"
393
- f"Список товаров: {products_list_str}\n"
394
- f"Общая сумма: {total_price}\n"
395
- f"Текущая дата: {datetime.now().strftime('%Y-%m-%d')}"
396
- )
397
 
 
398
  try:
399
- model = genai.GenerativeModel('learnlm-2.0-flash-experimental')
400
- response = model.generate_content(prompt)
401
-
402
- if hasattr(response, 'text'):
403
- return response.text
404
- elif response.parts:
405
- return "".join(part.text for part in response.parts if hasattr(part, 'text'))
406
- else:
407
- response.resolve()
408
- return response.text
409
 
 
 
 
 
 
 
 
 
 
 
 
410
  except Exception as e:
411
- if "API key not valid" in str(e):
412
- raise ValueError("Внутренняя ошибка конфигурации API.")
413
- elif " Billing account not found" in str(e):
414
- raise ValueError("Проблема с биллингом аккаунта Google Cloud. Проверьте ваш аккаунт.")
415
- elif "Could not find model" in str(e):
416
- raise ValueError(f"Модель 'learnlm-2.0-flash-experimental' не найдена или недоступна.")
417
- elif "resource has been exhausted" in str(e).lower():
418
- raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
419
- elif "content has been blocked" in str(e).lower():
420
- reason = "неизвестна"
421
- if hasattr(e, 'response') and hasattr(e.response, 'prompt_feedback') and e.response.prompt_feedback.block_reason:
422
- reason = e.response.prompt_feedback.block_reason
423
- raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте изменить запрос.")
424
- else:
425
- raise ValueError(f"Ошибка при генерации контента: {e}")
426
 
427
  CATALOG_TEMPLATE = '''
428
  <!DOCTYPE html>
@@ -657,7 +618,6 @@ CATALOG_TEMPLATE = '''
657
  #chat-send-button { background-color: #0070D1; color: white; border: none; border-radius: 20px; width: 40px; height: 40px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: background-color 0.3s; }
658
  #chat-send-button:hover { background-color: #005CBF; }
659
  #chat-send-button:disabled { background-color: #cccccc; cursor: not-allowed; }
660
-
661
  .chat-product-card { background-color: #f0f2f5; border-radius: 12px; padding: 10px; margin-top: 8px; display: flex; align-items: center; gap: 12px; border: 1px solid #e0e0e0; }
662
  .chat-product-card img { width: 50px; height: 50px; object-fit: cover; border-radius: 8px; flex-shrink: 0; }
663
  .chat-product-card-info { flex-grow: 1; }
@@ -671,7 +631,6 @@ CATALOG_TEMPLATE = '''
671
  }
672
  .chat-product-link:hover, .chat-add-to-cart:hover { background-color: #BBDEFB; }
673
  .chat-product-card-actions .fa-cart-plus { font-size: 0.9em; }
674
-
675
  </style>
676
  </head>
677
  <body>
@@ -1328,10 +1287,9 @@ ORDER_TEMPLATE = '''
1328
  .order-summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid #0070D1; text-align: right; }
1329
  .order-summary p { margin-bottom: 10px; font-size: 1.1rem; }
1330
  .order-summary strong { font-size: 1.3rem; color: #0A2A66; }
1331
- .customer-info-form { margin-top: 30px; background-color: #f9f9f9; padding: 20px; border-radius: 8px; border: 1px solid #e0e0e0;}
1332
- .customer-info-form label { display: block; margin-bottom: 5px; font-weight: 500;}
1333
- .customer-info-form input[type="text"], .customer-info-form input[type="tel"] { width: 100%; padding: 10px; margin-bottom: 15px; border: 1px solid #e0e0e0; border-radius: 6px; box-sizing: border-box; }
1334
- .customer-info-form input[type="tel"]::placeholder { color: #aaa; }
1335
  .actions { margin-top: 30px; text-align: center; }
1336
  .button { padding: 12px 25px; border: none; border-radius: 8px; background-color: #0070D1; color: white; font-weight: 600; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; font-size: 1rem; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; }
1337
  .button:hover { background-color: #005CBF; }
@@ -1369,85 +1327,28 @@ ORDER_TEMPLATE = '''
1369
  <p><strong>ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
1370
  </div>
1371
 
1372
- <div class="customer-info-form">
1373
- <h2><i class="fas fa-user"></i> Контактная информация</h2>
1374
- {% if order.user_info and order.user_info.name and order.user_info.whatsapp_number %}
1375
- <p><strong>Имя:</strong> {{ order.user_info.name }}</p>
1376
- <p><strong>WhatsApp:</strong> {{ order.user_info.whatsapp_number }}</p>
1377
- <p style="font-size: 0.9rem; color: #666; margin-top: 15px;">Мы свяжемся с вами для подтверждения заказа.</p>
1378
- {% else %}
1379
- <p>Для подтверждения заказа и уточнения деталей, пожалуйста, укажите ваше имя и номер WhatsApp:</p>
1380
- <label for="customer_name">Ваше имя:</label>
1381
- <input type="text" id="customer_name" placeholder="Иван Иванов" required>
1382
- <label for="customer_whatsapp">Ваш номер WhatsApp (начиная с +):</label>
1383
- <input type="tel" id="customer_whatsapp" placeholder="+996XXXXXXXXX" required pattern="^\\+?[0-9]{7,15}$">
1384
- {% endif %}
1385
  </div>
1386
 
1387
  <div class="actions">
1388
- <button class="button" onclick="sendOrderViaWhatsApp('{{ order.id }}')"><i class="fab fa-whatsapp"></i> Отправить заказ</button>
1389
  </div>
1390
 
1391
  <a href="{{ url_for('catalog') }}" class="catalog-link">← Вернуться в каталог</a>
1392
 
1393
  <script>
1394
- async function sendOrderViaWhatsApp(orderId) {
1395
- const customerNameInput = document.getElementById('customer_name');
1396
- const customerWhatsappInput = document.getElementById('customer_whatsapp');
1397
- let customerName = customerNameInput ? customerNameInput.value.trim() : '{{ order.user_info.name if order.user_info else '' }}';
1398
- let customerWhatsapp = customerWhatsappInput ? customerWhatsappInput.value.trim() : '{{ order.user_info.whatsapp_number if order.user_info else '' }}';
1399
-
1400
- if (!customerName && customerNameInput) {
1401
- alert("Пожалуйста, введите ваше имя.");
1402
- customerNameInput.focus();
1403
- return;
1404
- }
1405
- if (!customerWhatsapp && customerWhatsappInput) {
1406
- alert("Пожалуйста, введите ваш номер WhatsApp.");
1407
- customerWhatsappInput.focus();
1408
- return;
1409
- }
1410
- if (customerWhatsapp && customerWhatsappInput && !customerWhatsappInput.checkValidity()) {
1411
- alert("Пожалуйста, введите корректный номер WhatsApp (начиная с +).");
1412
- customerWhatsappInput.focus();
1413
- return;
1414
- }
1415
-
1416
- if (customerNameInput && customerWhatsappInput) {
1417
- try {
1418
- const updateResponse = await fetch(`/order/${orderId}/update_customer_info`, {
1419
- method: 'POST',
1420
- headers: { 'Content-Type': 'application/json' },
1421
- body: JSON.stringify({ name: customerName, whatsapp_number: customerWhatsapp })
1422
- });
1423
- if (!updateResponse.ok) {
1424
- const errorData = await updateResponse.json();
1425
- throw new Error(errorData.error || 'Не удалось обновить информацию о клиенте.');
1426
- }
1427
- const customerInfoForm = document.querySelector('.customer-info-form');
1428
- if (customerInfoForm) {
1429
- customerInfoForm.innerHTML = `
1430
- <h2><i class="fas fa-user"></i> Контактная информация</h2>
1431
- <p><strong>Имя:</strong> ${customerName}</p>
1432
- <p><strong>WhatsApp:</strong> ${customerWhatsapp}</p>
1433
- <p style="font-size: 0.9rem; color: #666; margin-top: 15px;">Мы свяжемся с вами для подтверждения заказа.</p>
1434
- `;
1435
- }
1436
- } catch (error) {
1437
- console.error('Ошибка при обновлении информации о клиенте:', error);
1438
- alert(`Ошибка: ${error.message}`);
1439
- return;
1440
- }
1441
- }
1442
-
1443
- const whatsappNumber = "{{ whatsapp_number }}";
1444
  const orderUrl = `{{ request.url }}`;
 
1445
 
1446
- let message = `Здравствуйте! Хочу подтвердить свой заказ на OCO (ID: ${orderId}).`;
1447
- if (customerName) {
1448
- message += ` Меня зовут ${customerName}.`;
1449
- }
1450
- message += `%0A%0AСсылка на заказ: ${encodeURIComponent(orderUrl)}%0A%0AПожалуйста, свяжитесь со мной для уточнения деталей оплаты и доставки.`;
1451
 
1452
  const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${message}`;
1453
  window.open(whatsappUrl, '_blank');
@@ -1512,7 +1413,7 @@ ADMIN_TEMPLATE = '''
1512
  details { background-color: #fdfdff; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; }
1513
  details > summary { cursor: pointer; font-weight: 600; color: #303F9F; display: block; padding: 15px; border-bottom: 1px solid #e0e0e0; list-style: none; position: relative; }
1514
  details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; color: #3F51B5; }
1515
- details[open] > summary::after { content: '\\f077'; }
1516
  details[open] > summary { border-bottom: 1px solid #e0e0e0; }
1517
  details .form-content { padding: 20px; }
1518
  .color-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
@@ -1537,24 +1438,7 @@ ADMIN_TEMPLATE = '''
1537
  .status-indicator.top-product { background-color: #fff3cd; color: #856404; margin-left: 5px;}
1538
  .ai-generate-button { background-color: #8D6EC8; margin-top: 5px; margin-bottom: 10px; }
1539
  .ai-generate-button:hover { background-color: #7B4DB5; }
1540
-
1541
- .order-item-admin { display: grid; grid-template-columns: 100px 1fr 150px; gap: 15px; padding: 15px 0; border-bottom: 1px solid #f0f0f0; align-items: center;}
1542
- .order-item-admin:last-child { border-bottom: none; }
1543
- .order-item-admin .order-id { font-weight: bold; font-size: 1.1rem; color: #333; }
1544
- .order-item-admin .order-meta-info p { margin: 3px 0; font-size: 0.85rem; color: #666; }
1545
- .order-item-admin .order-actions button { margin-top: 0; padding: 8px 12px; font-size: 0.8rem; }
1546
- .order-details-modal { max-width: 600px; }
1547
- .order-details-modal h3 { font-size: 1.3rem; margin-bottom: 15px; }
1548
- .order-details-modal .detail-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px dotted #eee; }
1549
- .order-details-modal .detail-row:last-child { border-bottom: none; }
1550
- .order-details-modal .detail-row strong { color: #004282; }
1551
- .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(5px); overflow-y: auto; }
1552
- .modal-content { background: #ffffff; color: #333; margin: 5% auto; padding: 25px; border-radius: 15px; width: 90%; max-width: 700px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); animation: slideIn 0.3s ease-out; position: relative; }
1553
- .close { position: absolute; top: 15px; right: 15px; font-size: 1.8rem; color: #aaa; cursor: pointer; transition: color 0.3s; line-height: 1; }
1554
- .close:hover { color: #666; }
1555
- @keyframes slideIn { from { transform: translateY(-30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
1556
- .modal-content h2 { margin-top: 0; margin-bottom: 20px; color: #0A2A66; display: flex; align-items: center; gap: 10px;}
1557
- .modal-content h3 { font-size: 1.3rem; margin-top: 20px; margin-bottom: 10px; color: #004282;}
1558
  </style>
1559
  </head>
1560
  <body>
@@ -1582,12 +1466,39 @@ ADMIN_TEMPLATE = '''
1582
  <button type="submit" class="button" title="Загрузить локальные файлы на Hugging Face"><i class="fas fa-upload"></i> Загрузить БД</button>
1583
  </form>
1584
  <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно скачать данные с сервера? Это перезапишет ваши локальные файлы.');">
1585
- <button type="submit" class="button download-hf-button" title="Скачать файлы (перезапишет локальные)"><i class="fas fa-download"></i> Скачать БД</button>
1586
  </form>
1587
  </div>
1588
  <p style="font-size: 0.85rem; color: #999;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
1589
  </div>
1590
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1591
  <div class="flex-container">
1592
  <div class="flex-item">
1593
  <div class="section">
@@ -1826,50 +1737,6 @@ ADMIN_TEMPLATE = '''
1826
  {% endif %}
1827
  </div>
1828
 
1829
- <div class="section">
1830
- <h2><i class="fas fa-clipboard-list"></i> Управление заказами</h2>
1831
- {% if orders %}
1832
- <div class="item-list">
1833
- {% for order_id, order in orders.items() %}
1834
- <div class="item order-item-admin">
1835
- <div class="order-id">
1836
- №{{ order.id }}
1837
- </div>
1838
- <div class="order-meta-info">
1839
- <p><strong>Дата:</strong> {{ order.created_at }}</p>
1840
- <p><strong>Клиент:</strong> {{ order.user_info.name if order.user_info and order.user_info.name else 'Не указано' }}</p>
1841
- <p><strong>WhatsApp:</strong> {{ order.user_info.whatsapp_number if order.user_info and order.user_info.whatsapp_number else 'Не указано' }}</p>
1842
- <p><strong>Сумма:</strong> {{ "%.2f"|format(order.total_price) }} {{ currency_code }}</p>
1843
- <p><strong>Товаров:</strong> {{ order.cart|length }}</p>
1844
- </div>
1845
- <div class="order-actions">
1846
- <button class="button" onclick="openOrderDetailsModal('{{ order.id }}')"><i class="fas fa-info"></i> Детали</button>
1847
- {% if order.user_info and order.user_info.whatsapp_number %}
1848
- <button class="button ai-generate-button" onclick="generateAndSendWhatsAppMessage('{{ order.id }}', '{{ order.user_info.name if order.user_info.name else '' }}', '{{ order.user_info.whatsapp_number }}')">
1849
- <i class="fab fa-whatsapp"></i> AI-сообщение
1850
- </button>
1851
- {% endif %}
1852
- <form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить заказ №{{ order.id }}?');">
1853
- <input type="hidden" name="action" value="delete_order">
1854
- <input type="hidden" name="order_id" value="{{ order.id }}">
1855
- <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
1856
- </form>
1857
- </div>
1858
- </div>
1859
- {% endfor %}
1860
- </div>
1861
- {% else %}
1862
- <p>Заказов пока нет.</p>
1863
- {% endif %}
1864
- </div>
1865
-
1866
- </div>
1867
-
1868
- <div id="orderDetailsModal" class="modal">
1869
- <div class="modal-content order-details-modal">
1870
- <span class="close" onclick="closeModal('orderDetailsModal')" aria-label="Закрыть">×</span>
1871
- <div id="modalOrderDetailsContent">Загрузка...</div>
1872
- </div>
1873
  </div>
1874
 
1875
  <script>
@@ -1920,7 +1787,7 @@ ADMIN_TEMPLATE = '''
1920
  const photoInput = document.getElementById(photoInputId);
1921
  const descriptionTextarea = document.getElementById(descriptionTextareaId);
1922
  const languageSelect = document.getElementById(languageSelectId);
1923
- const generateButton = descriptionTextarea.nextElementSibling;
1924
 
1925
  if (!photoInput || !descriptionTextarea || !languageSelect || !generateButton) {
1926
  alert("Ошибка: Не найдены элементы формы для генерации.");
@@ -1980,105 +1847,11 @@ ADMIN_TEMPLATE = '''
1980
  };
1981
  reader.readAsDataURL(file);
1982
  }
1983
-
1984
- function openModal(modalId) {
1985
- const modal = document.getElementById(modalId);
1986
- if (modal) {
1987
- modal.style.display = "block";
1988
- document.body.style.overflow = 'hidden';
1989
- }
1990
- }
1991
-
1992
- function closeModal(modalId) {
1993
- const modal = document.getElementById(modalId);
1994
- if (modal) {
1995
- modal.style.display = "none";
1996
- }
1997
- const anyModalOpen = document.querySelector('.modal[style*="display: block"]');
1998
- if (!anyModalOpen) {
1999
- document.body.style.overflow = 'auto';
2000
- }
2001
- }
2002
-
2003
- async function openOrderDetailsModal(orderId) {
2004
- const modalContent = document.getElementById('modalOrderDetailsContent');
2005
- if (!modalContent) return;
2006
- modalContent.innerHTML = '<p style="text-align:center; padding: 40px;">Загрузка...</p>';
2007
- try {
2008
- const response = await fetch(`/admin/order_details_html/${orderId}`);
2009
- if (!response.ok) {
2010
- throw new Error(`Ошибка ${response.status}: ${response.statusText}`);
2011
- }
2012
- const html = await response.text();
2013
- modalContent.innerHTML = html;
2014
- openModal('orderDetailsModal');
2015
- } catch (error) {
2016
- console.error('Ошибка загрузки деталей заказа:', error);
2017
- modalContent.innerHTML = `<p style="color: #dc3545; text-align:center; padding: 40px;">Не удалось загрузить информацию о заказе. ${error.message}</p>`;
2018
- }
2019
- }
2020
-
2021
- async function generateAndSendWhatsAppMessage(orderId, customerName, customerWhatsapp) {
2022
- const button = event.currentTarget;
2023
- button.disabled = true;
2024
- const originalText = button.innerHTML;
2025
- button.innerHTML = '<i class="fas fa-sync fa-spin"></i> Генерация...';
2026
-
2027
- try {
2028
- const response = await fetch(`/admin/generate_whatsapp_message/${orderId}`);
2029
- if (!response.ok) {
2030
- const errorData = await response.json();
2031
- throw new Error(errorData.error || `Ошибка сервера: ${response.status}`);
2032
- }
2033
- const result = await response.json();
2034
- const aiMessage = result.message;
2035
-
2036
- let finalMessage = `${aiMessage}`;
2037
-
2038
- const whatsappUrl = `https://api.whatsapp.com/send?phone=${customerWhatsapp}&text=${encodeURIComponent(finalMessage)}`;
2039
- window.open(whatsappUrl, '_blank');
2040
- } catch (error) {
2041
- console.error('Ошибка генерации WhatsApp сообщения:', error);
2042
- alert(`Ошибка: ${error.message}`);
2043
- } finally {
2044
- button.disabled = false;
2045
- button.innerHTML = originalText;
2046
- }
2047
- }
2048
  </script>
2049
  </body>
2050
  </html>
2051
  '''
2052
 
2053
- ADMIN_ORDER_DETAILS_MODAL_HTML = '''
2054
- <div style="padding: 10px;">
2055
- <h3>Детали Заказа №{{ order.id }}</h3>
2056
- <div class="detail-row"><strong>Дата создания:</strong> <span>{{ order.created_at }}</span></div>
2057
- <div class="detail-row"><strong>Статус:</strong> <span>{{ order.status }}</span></div>
2058
- <div class="detail-row"><strong>Клиент:</strong> <span>{{ order.user_info.name if order.user_info and order.user_info.name else 'Не указано' }}</span></div>
2059
- <div class="detail-row"><strong>WhatsApp:</strong> <span>{{ order.user_info.whatsapp_number if order.user_info and order.user_info.whatsapp_number else 'Не указано' }}</span></div>
2060
- <div class="detail-row"><strong>Общая сумма:</strong> <span>{{ "%.2f"|format(order.total_price) }} {{ currency_code }}</span></div>
2061
-
2062
- <h3 style="margin-top: 25px;">Товары:</h3>
2063
- {% if order.cart %}
2064
- {% for item in order.cart %}
2065
- <div style="display: flex; gap: 10px; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px dotted #eee; align-items: center;">
2066
- <img src="{{ item.photo_url }}" alt="{{ item.name }}" style="width: 50px; height: 50px; object-fit: contain; border-radius: 5px;">
2067
- <div>
2068
- <strong>{{ item.name }} {% if item.color != 'N/A' %}({{ item.color }}){% endif %}</strong><br>
2069
- <span>{{ "%.2f"|format(item.price) }} {{ currency_code }} × {{ item.quantity }}</span>
2070
- </div>
2071
- <div style="margin-left: auto; font-weight: bold;">
2072
- {{ "%.2f"|format(item.price * item.quantity) }} {{ currency_code }}
2073
- </div>
2074
- </div>
2075
- {% endfor %}
2076
- {% else %}
2077
- <p>Товары в заказе отсутствуют.</p>
2078
- {% endif %}
2079
- </div>
2080
- '''
2081
-
2082
  @app.route('/')
2083
  def catalog():
2084
  data = load_data()
@@ -2139,7 +1912,6 @@ def create_order():
2139
  return jsonify({"error": "Корзина пуста или не передана."}), 400
2140
 
2141
  cart_items = order_data['cart']
2142
-
2143
  total_price = 0
2144
  processed_cart = []
2145
  for item in cart_items:
@@ -2179,76 +1951,22 @@ def create_order():
2179
  data = load_data()
2180
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
2181
  data['orders'] = {}
2182
-
2183
  data['orders'][order_id] = new_order
2184
  save_data(data)
2185
  return jsonify({"order_id": order_id}), 201
2186
-
2187
  except Exception as e:
2188
  return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
2189
 
2190
-
2191
  @app.route('/order/<order_id>')
2192
  def view_order(order_id):
2193
  data = load_data()
2194
  order = data.get('orders', {}).get(order_id)
2195
-
2196
  return render_template_string(ORDER_TEMPLATE,
2197
  order=order,
2198
  repo_id=REPO_ID,
2199
  currency_code=CURRENCY_CODE,
2200
  whatsapp_number=WHATSAPP_NUMBER)
2201
 
2202
- @app.route('/order/<order_id>/update_customer_info', methods=['POST'])
2203
- def update_order_customer_info(order_id):
2204
- data = load_data()
2205
- order = data.get('orders', {}).get(order_id)
2206
-
2207
- if not order:
2208
- return jsonify({"error": "Заказ не найден."}), 404
2209
-
2210
- customer_info = request.get_json()
2211
- name = customer_info.get('name', '').strip()
2212
- whatsapp_number = customer_info.get('whatsapp_number', '').strip()
2213
-
2214
- if not name or not whatsapp_number:
2215
- return jsonify({"error": "Имя и номер WhatsApp обязательны."}), 400
2216
-
2217
- order['user_info'] = {
2218
- 'name': name,
2219
- 'whatsapp_number': whatsapp_number
2220
- }
2221
- save_data(data)
2222
- return jsonify({"message": "Информация о клиенте успешно обновлена."}), 200
2223
-
2224
- @app.route('/admin/order_details_html/<order_id>')
2225
- def admin_order_details_html(order_id):
2226
- data = load_data()
2227
- order = data.get('orders', {}).get(order_id)
2228
- if not order:
2229
- return "<p style='text-align: center; color: #dc3545;'>Заказ не найден.</p>", 404
2230
- return render_template_string(ADMIN_ORDER_DETAILS_MODAL_HTML, order=order, currency_code=CURRENCY_CODE)
2231
-
2232
- @app.route('/admin/generate_whatsapp_message/<order_id>')
2233
- def generate_admin_whatsapp_message(order_id):
2234
- data = load_data()
2235
- order = data.get('orders', {}).get(order_id)
2236
-
2237
- if not order:
2238
- return jsonify({"error": "Заказ не найден."}), 404
2239
-
2240
- customer_whatsapp = order.get('user_info', {}).get('whatsapp_number')
2241
- if not customer_whatsapp:
2242
- return jsonify({"error": "Номер WhatsApp клиента не указан для этого заказа."}), 400
2243
-
2244
- try:
2245
- ai_message = generate_order_whatsapp_message_ai(order)
2246
- return jsonify({"message": ai_message})
2247
- except ValueError as ve:
2248
- return jsonify({"error": str(ve)}), 400
2249
- except Exception as e:
2250
- return jsonify({"error": f"Ошибка генерации сообщения: {e}"}), 500
2251
-
2252
 
2253
  @app.route('/admin', methods=['GET', 'POST'])
2254
  def admin():
@@ -2256,11 +1974,13 @@ def admin():
2256
  products = data.get('products', [])
2257
  categories = data.get('categories', [])
2258
  organization_info = data.get('organization_info', {})
2259
- orders = data.get('orders', {})
 
 
 
2260
 
2261
  if request.method == 'POST':
2262
  action = request.form.get('action')
2263
-
2264
  try:
2265
  if action == 'add_category':
2266
  category_name = request.form.get('category_name', '').strip()
@@ -2270,9 +1990,9 @@ def admin():
2270
  save_data(data)
2271
  flash(f"Категория '{category_name}' успешно добавлена.", 'success')
2272
  elif not category_name:
2273
- flash("Название категории не может быть пустым.", 'error')
2274
  else:
2275
- flash(f"Категория '{category_name}' уже существует.", 'error')
2276
 
2277
  elif action == 'delete_category':
2278
  category_to_delete = request.form.get('category_name')
@@ -2298,6 +2018,16 @@ def admin():
2298
  data['organization_info'] = organization_info
2299
  save_data(data)
2300
  flash("Информация о магазине успешно обновлена.", 'success')
 
 
 
 
 
 
 
 
 
 
2301
 
2302
  elif action == 'add_product':
2303
  name = request.form.get('name', '').strip()
@@ -2312,13 +2042,12 @@ def admin():
2312
  if not name or not price_str:
2313
  flash("Название и цена товара обязательны.", 'error')
2314
  return redirect(url_for('admin'))
2315
-
2316
  try:
2317
  price = round(float(price_str), 2)
2318
  if price < 0: price = 0
2319
  except ValueError:
2320
- flash("Неверный формат цены.", 'error')
2321
- return redirect(url_for('admin'))
2322
 
2323
  photos_list = []
2324
  if photos_files and HF_TOKEN_WRITE:
@@ -2335,9 +2064,8 @@ def admin():
2335
  try:
2336
  ext = os.path.splitext(photo.filename)[1].lower()
2337
  if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
2338
- flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
2339
- continue
2340
-
2341
  safe_name = secure_filename(name.replace(' ', '_'))[:50]
2342
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
2343
  temp_path = os.path.join(uploads_dir, photo_filename)
@@ -2359,16 +2087,15 @@ def admin():
2359
  try: os.remove(temp_path)
2360
  except OSError: pass
2361
  elif photo and not photo.filename:
2362
- pass
2363
  try:
2364
- if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
2365
- os.rmdir(uploads_dir)
2366
  except OSError as e:
2367
  pass
2368
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
2369
  flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
2370
 
2371
-
2372
  new_product = {
2373
  'product_id': uuid4().hex,
2374
  'name': name, 'price': price, 'description': description,
@@ -2384,13 +2111,10 @@ def admin():
2384
  elif action == 'edit_product':
2385
  product_id = request.form.get('product_id')
2386
  product_to_edit = next((p for p in products if p.get('product_id') == product_id), None)
2387
-
2388
  if product_to_edit is None:
2389
- flash(f"Ошибка редактирования: товар с ID '{product_id}' не найден.", 'error')
2390
- return redirect(url_for('admin'))
2391
-
2392
  original_name = product_to_edit.get('name', 'N/A')
2393
-
2394
  product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
2395
  price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
2396
  product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
@@ -2399,13 +2123,12 @@ def admin():
2399
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
2400
  product_to_edit['in_stock'] = 'in_stock' in request.form
2401
  product_to_edit['is_top'] = 'is_top' in request.form
2402
-
2403
  try:
2404
  price = round(float(price_str), 2)
2405
  if price < 0: price = 0
2406
  product_to_edit['price'] = price
2407
  except ValueError:
2408
- flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
2409
 
2410
  photos_files = request.files.getlist('photos')
2411
  if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE:
@@ -2423,9 +2146,8 @@ def admin():
2423
  try:
2424
  ext = os.path.splitext(photo.filename)[1].lower()
2425
  if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
2426
- flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
2427
- continue
2428
-
2429
  safe_name = secure_filename(product_to_edit['name'].replace(' ', '_'))[:50]
2430
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
2431
  temp_path = os.path.join(uploads_dir, photo_filename)
@@ -2439,11 +2161,11 @@ def admin():
2439
  except Exception as e:
2440
  flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
2441
  if os.path.exists(temp_path):
2442
- try: os.remove(temp_path)
2443
- except OSError: pass
2444
  try:
2445
- if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
2446
- os.rmdir(uploads_dir)
2447
  except OSError as e:
2448
  pass
2449
 
@@ -2464,24 +2186,20 @@ def admin():
2464
  product_to_edit['photos'] = new_photos_list
2465
  flash("Фотографии товара успешно обновлены.", "success")
2466
  elif uploaded_count == 0 and any(f.filename for f in photos_files):
2467
- flash("Не удалось загрузить новые фотографии (возможно, неверный формат).", "error")
2468
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
2469
  flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning")
2470
-
2471
  save_data(data)
2472
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
2473
 
2474
  elif action == 'delete_product':
2475
  product_id = request.form.get('product_id')
2476
  product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1)
2477
-
2478
  if product_index == -1:
2479
  flash(f"Ошибка удаления: товар с ID '{product_id}' не найден.", 'error')
2480
  return redirect(url_for('admin'))
2481
-
2482
  deleted_product = products.pop(product_index)
2483
  product_name = deleted_product.get('name', 'N/A')
2484
-
2485
  photos_to_delete = deleted_product.get('photos', [])
2486
  if photos_to_delete and HF_TOKEN_WRITE:
2487
  try:
@@ -2494,48 +2212,62 @@ def admin():
2494
  commit_message=f"Delete photos for deleted product {product_name}"
2495
  )
2496
  except Exception as e:
2497
- flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning")
2498
  elif photos_to_delete and not HF_TOKEN_WRITE:
2499
  flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
2500
-
2501
  data['products'] = products
2502
  save_data(data)
2503
  flash(f"Товар '{product_name}' удален.", 'success')
2504
-
2505
- elif action == 'delete_order':
2506
- order_id_to_delete = request.form.get('order_id')
2507
- if order_id_to_delete in orders:
2508
- del orders[order_id_to_delete]
2509
- data['orders'] = orders
2510
- save_data(data)
2511
- flash(f"Заказ №{order_id_to_delete} успешно удален.", 'success')
2512
- else:
2513
- flash(f"Заказ №{order_id_to_delete} не найден.", 'error')
2514
-
2515
  else:
2516
- flash(f"Неизвестное действие: {action}", 'warning')
2517
-
2518
  return redirect(url_for('admin'))
2519
-
2520
  except Exception as e:
2521
- flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'.", 'error')
2522
- return redirect(url_for('admin'))
2523
 
2524
  current_data = load_data()
2525
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
2526
  display_categories = sorted(current_data.get('categories', []))
2527
  display_organization_info = current_data.get('organization_info', {})
2528
- display_orders = dict(sorted(current_data.get('orders', {}).items(), key=lambda item: item[1]['created_at'], reverse=True))
 
 
 
 
2529
 
2530
  return render_template_string(
2531
  ADMIN_TEMPLATE,
2532
  products=display_products,
2533
  categories=display_categories,
2534
  organization_info=display_organization_info,
2535
- orders=display_orders,
 
2536
  repo_id=REPO_ID,
2537
  currency_code=CURRENCY_CODE
2538
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2539
 
2540
  @app.route('/generate_description_ai', methods=['POST'])
2541
  def handle_generate_description_ai():
@@ -2545,7 +2277,6 @@ def handle_generate_description_ai():
2545
 
2546
  if not base64_image:
2547
  return jsonify({"error": "Изображение не найдено в запросе."}), 400
2548
-
2549
  try:
2550
  image_data = base64.b64decode(base64_image)
2551
  result_text = generate_ai_description_from_image(image_data, language)
@@ -2563,7 +2294,6 @@ def handle_chat_with_ai():
2563
 
2564
  if not user_message:
2565
  return jsonify({"error": "Сообщение не может быть пустым."}), 400
2566
-
2567
  try:
2568
  ai_response_text = generate_chat_response(user_message, chat_history_from_client)
2569
  return jsonify({"text": ai_response_text})
 
41
  DOWNLOAD_RETRIES = 3
42
  DOWNLOAD_DELAY = 5
43
 
44
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
45
+
46
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
47
  if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
48
+ logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
 
49
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
 
50
  files_to_download = [specific_file] if specific_file else SYNC_FILES
51
  all_successful = True
 
52
  for file_name in files_to_download:
53
  success = False
54
  for attempt in range(retries + 1):
 
66
  success = True
67
  break
68
  except RepositoryNotFoundError:
69
+ return False
70
  except HfHubHTTPError as e:
71
  if e.response.status_code == 404:
72
  if attempt == 0 and not os.path.exists(file_name):
 
81
  else:
82
  pass
83
  except requests.exceptions.RequestException as e:
84
+ pass
85
  except Exception as e:
86
+ pass
 
87
  if attempt < retries:
88
  time.sleep(delay)
 
89
  if not success:
90
  all_successful = False
 
91
  return all_successful
92
 
93
  def upload_db_to_hf(specific_file=None):
 
96
  try:
97
  api = HfApi()
98
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
 
99
  for file_name in files_to_upload:
100
  if os.path.exists(file_name):
101
  try:
 
108
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
109
  )
110
  except Exception as e:
111
+ pass
112
  else:
113
  pass
114
  except Exception as e:
 
127
  "returns": "Возврат и обмен товара возможен в течение 14 дней с момента покупки, при условии сохранения товарного вида, упаковки и чека. Некоторые категории товаров могут иметь особые условия возврата. Пожалуйста, свяжитесь с нами для оформления возврата или обмена.",
128
  "contact": f"Наш магазин находится по адресу: {STORE_ADDRESS}. Связаться с нами можно по телефону: {WHATSAPP_NUMBER} или через WhatsApp по этому же номеру. Мы работаем ежедневно с 9:00 до 18:00."
129
  }
130
+ default_whatsapp_config = {'enabled': False, 'gateway_url': '', 'token': '', 'webhook_secret': uuid4().hex}
131
+ default_data = {'products': [], 'categories': [], 'orders': {}, 'organization_info': default_organization_info, 'whatsapp_config': default_whatsapp_config}
132
+
133
  data = default_data
134
  try:
135
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
136
  data = json.load(file)
137
  if not isinstance(data, dict):
138
+ raise FileNotFoundError
139
  if 'products' not in data: data['products'] = []
140
  if 'categories' not in data: data['categories'] = []
141
  if 'orders' not in data: data['orders'] = {}
142
  if 'organization_info' not in data: data['organization_info'] = default_organization_info
143
+ if 'whatsapp_config' not in data: data['whatsapp_config'] = default_whatsapp_config
144
+ if 'webhook_secret' not in data.get('whatsapp_config', {}): data['whatsapp_config']['webhook_secret'] = uuid4().hex
145
  except FileNotFoundError:
146
  if download_db_from_hf(specific_file=DATA_FILE):
147
  try:
148
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
149
  data = json.load(file)
150
+ if not isinstance(data, dict): data = default_data
 
151
  if 'products' not in data: data['products'] = []
152
  if 'categories' not in data: data['categories'] = []
153
  if 'orders' not in data: data['orders'] = {}
154
  if 'organization_info' not in data: data['organization_info'] = default_organization_info
155
+ if 'whatsapp_config' not in data: data['whatsapp_config'] = default_whatsapp_config
156
+ if 'webhook_secret' not in data.get('whatsapp_config', {}): data['whatsapp_config']['webhook_secret'] = uuid4().hex
157
  except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
158
  data = default_data
159
  else:
 
163
  try:
164
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
165
  data = json.load(file)
166
+ if not isinstance(data, dict): data = default_data
 
167
  if 'products' not in data: data['products'] = []
168
  if 'categories' not in data: data['categories'] = []
169
  if 'orders' not in data: data['orders'] = {}
170
  if 'organization_info' not in data: data['organization_info'] = default_organization_info
171
+ if 'whatsapp_config' not in data: data['whatsapp_config'] = default_whatsapp_config
172
+ if 'webhook_secret' not in data.get('whatsapp_config', {}): data['whatsapp_config']['webhook_secret'] = uuid4().hex
173
  except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
174
  data = default_data
175
  else:
 
199
  if 'categories' not in data: data['categories'] = []
200
  if 'orders' not in data: data['orders'] = {}
201
  if 'organization_info' not in data: data['organization_info'] = {}
202
+ if 'whatsapp_config' not in data: data['whatsapp_config'] = {'enabled': False, 'gateway_url': '', 'token': '', 'webhook_secret': uuid4().hex}
203
 
204
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
205
  json.dump(data, file, ensure_ascii=False, indent=4)
 
222
 
223
  try:
224
  if not image_data:
225
+ raise ValueError("Файл изображения не найден.")
226
  image_stream = io.BytesIO(image_data)
227
  image = Image.open(image_stream).convert('RGB')
228
  except Exception as e:
 
243
  final_prompt = f"{base_prompt}{lang_suffix}"
244
 
245
  try:
246
+ model = genai.GenerativeModel('gemini-1.5-flash')
 
247
  response = model.generate_content([final_prompt, image])
 
248
  if hasattr(response, 'text'):
249
  return response.text
250
  else:
251
  if response.parts:
252
+ return "".join(part.text for part in response.parts if hasattr(part, 'text'))
253
  else:
254
  response.resolve()
255
  return response.text
 
256
  except Exception as e:
257
  if "API key not valid" in str(e):
258
+ raise ValueError("Внутренняя ошибка конфигурации API.")
259
  elif " Billing account not found" in str(e):
260
+ raise ValueError("Проблема с биллингом аккаунта Google Cloud. Проверьте ваш аккаунт.")
261
  elif "Could not find model" in str(e):
262
+ raise ValueError(f"Модель 'gemini-1.5-flash' не найдена или недоступна.")
263
  elif "resource has been exhausted" in str(e).lower():
264
+ raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
265
  elif "content has been blocked" in str(e).lower():
266
+ reason = "неизвестна"
267
+ if hasattr(e, 'response') and hasattr(e.response, 'prompt_feedback') and e.response.prompt_feedback.block_reason:
268
  reason = e.response.prompt_feedback.block_reason
269
+ raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другое изображение ил�� запрос.")
270
  else:
271
+ raise ValueError(f"Ошибка при генерации контента: {e}")
272
 
273
  def generate_chat_response(message, chat_history_from_client):
274
  if not configure_gemini():
 
300
  if organization_info.get("contact"):
301
  org_info_str += f"Контактная информация: {organization_info['contact']}\n"
302
 
 
303
  system_instruction_content = (
304
  "Ты - доброжелательный и очень полезный виртуальный консультант для магазина O&CO. "
305
  "Твоя задача - помогать пользователям находить товары, отвечать на вопросы о них, предлагать варианты, а также предоставлять информацию о магазине. "
 
321
  response = None
322
 
323
  try:
324
+ model = genai.GenerativeModel('gemini-1.5-flash', system_instruction=system_instruction_content)
325
 
326
  model_chat_history_for_gemini = []
327
  for entry in chat_history_from_client:
 
332
  })
333
 
334
  chat = model.start_chat(history=model_chat_history_for_gemini)
 
335
  response = chat.send_message(message, generation_config={'max_output_tokens': 1000})
336
 
337
  if hasattr(response, 'text'):
 
344
  generated_text = response.text
345
  else:
346
  raise ValueError("AI did not return a valid text response.")
 
347
  return generated_text
 
348
  except Exception as e:
349
  if "API key not valid" in str(e):
350
+ return "Внутренняя ошибка конфигурации API."
351
  elif " Billing account not found" in str(e):
352
+ return "Проблема с биллингом аккаунта Google Cloud."
353
  elif "Could not find model" in str(e):
354
+ return "Модель 'gemini-1.5-flash' не найдена или недоступна."
355
  elif "resource has been exhausted" in str(e).lower():
356
+ return "Квота запросов исчерпана. Попробуйте позже."
357
  elif "content has been blocked" in str(e).lower() or (response is not None and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason):
358
+ reason = response.prompt_feedback.block_reason if (response and hasattr(response, 'prompt_feedback')) else "неизвестна"
359
+ return f"Извините, Ваш запрос был заблокирован из-за политики безопасности (причина: {reason}). Пожалуйста, переформулируйте его."
360
  else:
361
+ return f"Извините, произошла ошибка: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
+ def process_whatsapp_message(from_number, user_message, config):
364
  try:
365
+ ai_response = generate_chat_response(user_message, [])
366
+
367
+ gateway_url = config.get('gateway_url')
368
+ token = config.get('token')
369
+
370
+ if not gateway_url:
371
+ logging.error("WhatsApp Gateway URL is not configured.")
372
+ return
 
 
373
 
374
+ headers = {'Content-Type': 'application/json'}
375
+ if token:
376
+ headers['Authorization'] = f"Bearer {token}"
377
+
378
+ reply_payload = {
379
+ "to": from_number,
380
+ "message": ai_response
381
+ }
382
+
383
+ requests.post(gateway_url, json=reply_payload, headers=headers, timeout=10)
384
+
385
  except Exception as e:
386
+ logging.error(f"Error processing WhatsApp message for {from_number}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
 
388
  CATALOG_TEMPLATE = '''
389
  <!DOCTYPE html>
 
618
  #chat-send-button { background-color: #0070D1; color: white; border: none; border-radius: 20px; width: 40px; height: 40px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: background-color 0.3s; }
619
  #chat-send-button:hover { background-color: #005CBF; }
620
  #chat-send-button:disabled { background-color: #cccccc; cursor: not-allowed; }
 
621
  .chat-product-card { background-color: #f0f2f5; border-radius: 12px; padding: 10px; margin-top: 8px; display: flex; align-items: center; gap: 12px; border: 1px solid #e0e0e0; }
622
  .chat-product-card img { width: 50px; height: 50px; object-fit: cover; border-radius: 8px; flex-shrink: 0; }
623
  .chat-product-card-info { flex-grow: 1; }
 
631
  }
632
  .chat-product-link:hover, .chat-add-to-cart:hover { background-color: #BBDEFB; }
633
  .chat-product-card-actions .fa-cart-plus { font-size: 0.9em; }
 
634
  </style>
635
  </head>
636
  <body>
 
1287
  .order-summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid #0070D1; text-align: right; }
1288
  .order-summary p { margin-bottom: 10px; font-size: 1.1rem; }
1289
  .order-summary strong { font-size: 1.3rem; color: #0A2A66; }
1290
+ .customer-info { margin-top: 30px; background-color: #f9f9f9; padding: 20px; border-radius: 8px; border: 1px solid #e0e0e0;}
1291
+ .customer-info p { margin-bottom: 8px; font-size: 0.95rem; }
1292
+ .customer-info strong { color: #004282; }
 
1293
  .actions { margin-top: 30px; text-align: center; }
1294
  .button { padding: 12px 25px; border: none; border-radius: 8px; background-color: #0070D1; color: white; font-weight: 600; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; font-size: 1rem; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; }
1295
  .button:hover { background-color: #005CBF; }
 
1327
  <p><strong>ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
1328
  </div>
1329
 
1330
+ <div class="customer-info">
1331
+ <h2><i class="fas fa-info-circle"></i> Статус заказа</h2>
1332
+ <p>Этот заказ был оформлен без входа в систему.</p>
1333
+ <p>Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.</p>
 
 
 
 
 
 
 
 
 
1334
  </div>
1335
 
1336
  <div class="actions">
1337
+ <button class="button" onclick="sendOrderViaWhatsApp()"><i class="fab fa-whatsapp"></i> Отправить заказ</button>
1338
  </div>
1339
 
1340
  <a href="{{ url_for('catalog') }}" class="catalog-link">← Вернуться в каталог</a>
1341
 
1342
  <script>
1343
+ function sendOrderViaWhatsApp() {
1344
+ const orderId = '{{ order.id }}';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1345
  const orderUrl = `{{ request.url }}`;
1346
+ const whatsappNumber = "{{ whatsapp_number }}";
1347
 
1348
+ let message = `Здравствуйте! Хочу подтвердить свой заказ на OCO:%0A%0A`;
1349
+ message += `*Номер заказа:* ${orderId}%0A`;
1350
+ message += `*Ссылка на заказ:* ${encodeURIComponent(orderUrl)}%0A%0A`;
1351
+ message += `Пожалуйста, свяжитесь со мной для уточнения деталей оплаты и доставки.`;
 
1352
 
1353
  const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${message}`;
1354
  window.open(whatsappUrl, '_blank');
 
1413
  details { background-color: #fdfdff; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; }
1414
  details > summary { cursor: pointer; font-weight: 600; color: #303F9F; display: block; padding: 15px; border-bottom: 1px solid #e0e0e0; list-style: none; position: relative; }
1415
  details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; color: #3F51B5; }
1416
+ details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
1417
  details[open] > summary { border-bottom: 1px solid #e0e0e0; }
1418
  details .form-content { padding: 20px; }
1419
  .color-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
 
1438
  .status-indicator.top-product { background-color: #fff3cd; color: #856404; margin-left: 5px;}
1439
  .ai-generate-button { background-color: #8D6EC8; margin-top: 5px; margin-bottom: 10px; }
1440
  .ai-generate-button:hover { background-color: #7B4DB5; }
1441
+ .webhook-url-display { padding: 10px; background-color: #e9ecef; border: 1px solid #ced4da; border-radius: 6px; font-family: monospace; word-wrap: break-word; font-size: 0.9rem; margin-top: 5px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1442
  </style>
1443
  </head>
1444
  <body>
 
1466
  <button type="submit" class="button" title="Загрузить локальные файлы на Hugging Face"><i class="fas fa-upload"></i> Загрузить БД</button>
1467
  </form>
1468
  <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно скачать данные с сервера? Это перезапишет ваши локальные файлы.');">
1469
+ <button type="submit" class="button download-hf-button" title="Скачать файлы (перезапишет локальные)"><i class="fas fa-download"></i> Скачать БД</button>
1470
  </form>
1471
  </div>
1472
  <p style="font-size: 0.85rem; color: #999;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
1473
  </div>
1474
 
1475
+ <div class="section">
1476
+ <h2><i class="fab fa-whatsapp"></i> Интеграция с WhatsApp</h2>
1477
+ <details>
1478
+ <summary><i class="fas fa-cog"></i> Настроить WhatsApp Бота</summary>
1479
+ <div class="form-content">
1480
+ <form method="POST">
1481
+ <input type="hidden" name="action" value="update_whatsapp_config">
1482
+ <div>
1483
+ <input type="checkbox" id="whatsapp_enabled" name="whatsapp_enabled" {% if whatsapp_config.get('enabled') %}checked{% endif %}>
1484
+ <label for="whatsapp_enabled" class="inline-label">Включить WhatsApp-бота</label>
1485
+ </div>
1486
+ <label for="whatsapp_gateway_url">URL шлюза для отправки сообщений:</label>
1487
+ <input type="text" id="whatsapp_gateway_url" name="whatsapp_gateway_url" value="{{ whatsapp_config.get('gateway_url', '') }}" placeholder="https://api.yourgateway.com/send">
1488
+ <label for="whatsapp_token">Токен авторизации для шлюза (Bearer Token):</label>
1489
+ <input type="text" id="whatsapp_token" name="whatsapp_token" value="{{ whatsapp_config.get('token', '') }}" placeholder="Ваш секретный токен">
1490
+
1491
+ <label>Ваш URL для вебхука (вставьте это в настройки шлюза):</label>
1492
+ <div class="webhook-url-display">{{ webhook_url }}</div>
1493
+ <p style="font-size: 0.85rem; color: #999; margin-top: 10px;">Ваш шлюз должен отправлять POST-запросы на этот URL с JSON-телом вида: <code>{"from": "79991234567", "message": "текст сообщения"}</code>.</p>
1494
+
1495
+ <button type="submit" class="add-button"><i class="fas fa-save"></i> Сохранить настройки WhatsApp</button>
1496
+ </form>
1497
+ <p style="font-size: 0.85rem; color: #999;"><b>Внимание:</b> "Серые" методы интеграции с WhatsApp могут привести к блокировке номера. Используйте на свой страх и риск.</p>
1498
+ </div>
1499
+ </details>
1500
+ </div>
1501
+
1502
  <div class="flex-container">
1503
  <div class="flex-item">
1504
  <div class="section">
 
1737
  {% endif %}
1738
  </div>
1739
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1740
  </div>
1741
 
1742
  <script>
 
1787
  const photoInput = document.getElementById(photoInputId);
1788
  const descriptionTextarea = document.getElementById(descriptionTextareaId);
1789
  const languageSelect = document.getElementById(languageSelectId);
1790
+ const generateButton = descriptionTextarea.nextElementSibling;
1791
 
1792
  if (!photoInput || !descriptionTextarea || !languageSelect || !generateButton) {
1793
  alert("Ошибка: Не найдены элементы формы для генерации.");
 
1847
  };
1848
  reader.readAsDataURL(file);
1849
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1850
  </script>
1851
  </body>
1852
  </html>
1853
  '''
1854
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1855
  @app.route('/')
1856
  def catalog():
1857
  data = load_data()
 
1912
  return jsonify({"error": "Корзина пуста или не передана."}), 400
1913
 
1914
  cart_items = order_data['cart']
 
1915
  total_price = 0
1916
  processed_cart = []
1917
  for item in cart_items:
 
1951
  data = load_data()
1952
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1953
  data['orders'] = {}
 
1954
  data['orders'][order_id] = new_order
1955
  save_data(data)
1956
  return jsonify({"order_id": order_id}), 201
 
1957
  except Exception as e:
1958
  return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
1959
 
 
1960
  @app.route('/order/<order_id>')
1961
  def view_order(order_id):
1962
  data = load_data()
1963
  order = data.get('orders', {}).get(order_id)
 
1964
  return render_template_string(ORDER_TEMPLATE,
1965
  order=order,
1966
  repo_id=REPO_ID,
1967
  currency_code=CURRENCY_CODE,
1968
  whatsapp_number=WHATSAPP_NUMBER)
1969
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1970
 
1971
  @app.route('/admin', methods=['GET', 'POST'])
1972
  def admin():
 
1974
  products = data.get('products', [])
1975
  categories = data.get('categories', [])
1976
  organization_info = data.get('organization_info', {})
1977
+ whatsapp_config = data.get('whatsapp_config', {})
1978
+
1979
+ if 'orders' not in data or not isinstance(data.get('orders'), dict):
1980
+ data['orders'] = {}
1981
 
1982
  if request.method == 'POST':
1983
  action = request.form.get('action')
 
1984
  try:
1985
  if action == 'add_category':
1986
  category_name = request.form.get('category_name', '').strip()
 
1990
  save_data(data)
1991
  flash(f"Категория '{category_name}' успешно добавлена.", 'success')
1992
  elif not category_name:
1993
+ flash("Название категории не может быть пустым.", 'error')
1994
  else:
1995
+ flash(f"Категория '{category_name}' уже существует.", 'error')
1996
 
1997
  elif action == 'delete_category':
1998
  category_to_delete = request.form.get('category_name')
 
2018
  data['organization_info'] = organization_info
2019
  save_data(data)
2020
  flash("Информация о магазине успешно обновлена.", 'success')
2021
+
2022
+ elif action == 'update_whatsapp_config':
2023
+ whatsapp_config['enabled'] = 'whatsapp_enabled' in request.form
2024
+ whatsapp_config['gateway_url'] = request.form.get('whatsapp_gateway_url', '').strip()
2025
+ whatsapp_config['token'] = request.form.get('whatsapp_token', '').strip()
2026
+ if 'webhook_secret' not in whatsapp_config or not whatsapp_config['webhook_secret']:
2027
+ whatsapp_config['webhook_secret'] = uuid4().hex
2028
+ data['whatsapp_config'] = whatsapp_config
2029
+ save_data(data)
2030
+ flash("Настройки WhatsApp успешно обновлены.", 'success')
2031
 
2032
  elif action == 'add_product':
2033
  name = request.form.get('name', '').strip()
 
2042
  if not name or not price_str:
2043
  flash("Название и цена товара обязательны.", 'error')
2044
  return redirect(url_for('admin'))
 
2045
  try:
2046
  price = round(float(price_str), 2)
2047
  if price < 0: price = 0
2048
  except ValueError:
2049
+ flash("Неверный формат цены.", 'error')
2050
+ return redirect(url_for('admin'))
2051
 
2052
  photos_list = []
2053
  if photos_files and HF_TOKEN_WRITE:
 
2064
  try:
2065
  ext = os.path.splitext(photo.filename)[1].lower()
2066
  if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
2067
+ flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
2068
+ continue
 
2069
  safe_name = secure_filename(name.replace(' ', '_'))[:50]
2070
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
2071
  temp_path = os.path.join(uploads_dir, photo_filename)
 
2087
  try: os.remove(temp_path)
2088
  except OSError: pass
2089
  elif photo and not photo.filename:
2090
+ pass
2091
  try:
2092
+ if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
2093
+ os.rmdir(uploads_dir)
2094
  except OSError as e:
2095
  pass
2096
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
2097
  flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
2098
 
 
2099
  new_product = {
2100
  'product_id': uuid4().hex,
2101
  'name': name, 'price': price, 'description': description,
 
2111
  elif action == 'edit_product':
2112
  product_id = request.form.get('product_id')
2113
  product_to_edit = next((p for p in products if p.get('product_id') == product_id), None)
 
2114
  if product_to_edit is None:
2115
+ flash(f"Ошибка редактирования: товар с ID '{product_id}' не найден.", 'error')
2116
+ return redirect(url_for('admin'))
 
2117
  original_name = product_to_edit.get('name', 'N/A')
 
2118
  product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
2119
  price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
2120
  product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
 
2123
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
2124
  product_to_edit['in_stock'] = 'in_stock' in request.form
2125
  product_to_edit['is_top'] = 'is_top' in request.form
 
2126
  try:
2127
  price = round(float(price_str), 2)
2128
  if price < 0: price = 0
2129
  product_to_edit['price'] = price
2130
  except ValueError:
2131
+ flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
2132
 
2133
  photos_files = request.files.getlist('photos')
2134
  if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE:
 
2146
  try:
2147
  ext = os.path.splitext(photo.filename)[1].lower()
2148
  if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
2149
+ flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
2150
+ continue
 
2151
  safe_name = secure_filename(product_to_edit['name'].replace(' ', '_'))[:50]
2152
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
2153
  temp_path = os.path.join(uploads_dir, photo_filename)
 
2161
  except Exception as e:
2162
  flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
2163
  if os.path.exists(temp_path):
2164
+ try: os.remove(temp_path)
2165
+ except OSError: pass
2166
  try:
2167
+ if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
2168
+ os.rmdir(uploads_dir)
2169
  except OSError as e:
2170
  pass
2171
 
 
2186
  product_to_edit['photos'] = new_photos_list
2187
  flash("Фотографии товара успешно обновлены.", "success")
2188
  elif uploaded_count == 0 and any(f.filename for f in photos_files):
2189
+ flash("Не удалось загрузить новые фотографии (возможно, неверный формат).", "error")
2190
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
2191
  flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning")
 
2192
  save_data(data)
2193
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
2194
 
2195
  elif action == 'delete_product':
2196
  product_id = request.form.get('product_id')
2197
  product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1)
 
2198
  if product_index == -1:
2199
  flash(f"Ошибка удаления: товар с ID '{product_id}' не найден.", 'error')
2200
  return redirect(url_for('admin'))
 
2201
  deleted_product = products.pop(product_index)
2202
  product_name = deleted_product.get('name', 'N/A')
 
2203
  photos_to_delete = deleted_product.get('photos', [])
2204
  if photos_to_delete and HF_TOKEN_WRITE:
2205
  try:
 
2212
  commit_message=f"Delete photos for deleted product {product_name}"
2213
  )
2214
  except Exception as e:
2215
+ flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning")
2216
  elif photos_to_delete and not HF_TOKEN_WRITE:
2217
  flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
 
2218
  data['products'] = products
2219
  save_data(data)
2220
  flash(f"Товар '{product_name}' удален.", 'success')
 
 
 
 
 
 
 
 
 
 
 
2221
  else:
2222
+ flash(f"Неизвестное действие: {action}", 'warning')
 
2223
  return redirect(url_for('admin'))
 
2224
  except Exception as e:
2225
+ flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
2226
+ return redirect(url_for('admin'))
2227
 
2228
  current_data = load_data()
2229
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
2230
  display_categories = sorted(current_data.get('categories', []))
2231
  display_organization_info = current_data.get('organization_info', {})
2232
+ display_whatsapp_config = current_data.get('whatsapp_config', {})
2233
+
2234
+ webhook_url = "Сначала сохраните настройки, чтобы сгенерировать URL."
2235
+ if display_whatsapp_config.get('webhook_secret'):
2236
+ webhook_url = f"{request.host_url}whatsapp_webhook/{display_whatsapp_config['webhook_secret']}"
2237
 
2238
  return render_template_string(
2239
  ADMIN_TEMPLATE,
2240
  products=display_products,
2241
  categories=display_categories,
2242
  organization_info=display_organization_info,
2243
+ whatsapp_config=display_whatsapp_config,
2244
+ webhook_url=webhook_url,
2245
  repo_id=REPO_ID,
2246
  currency_code=CURRENCY_CODE
2247
  )
2248
+
2249
+ @app.route('/whatsapp_webhook/<string:secret>', methods=['POST'])
2250
+ def whatsapp_webhook(secret):
2251
+ data = load_data()
2252
+ config = data.get('whatsapp_config', {})
2253
+
2254
+ if not config.get('enabled'):
2255
+ return jsonify({"error": "WhatsApp bot is disabled"}), 403
2256
+
2257
+ if not config.get('webhook_secret') or secret != config.get('webhook_secret'):
2258
+ return jsonify({"error": "Unauthorized"}), 401
2259
+
2260
+ payload = request.get_json()
2261
+ if not payload or 'from' not in payload or 'message' not in payload:
2262
+ return jsonify({"error": "Invalid payload. 'from' and 'message' fields are required."}), 400
2263
+
2264
+ from_number = payload['from']
2265
+ user_message = payload['message']
2266
+
2267
+ thread = threading.Thread(target=process_whatsapp_message, args=(from_number, user_message, config))
2268
+ thread.start()
2269
+
2270
+ return jsonify({"status": "received"}), 200
2271
 
2272
  @app.route('/generate_description_ai', methods=['POST'])
2273
  def handle_generate_description_ai():
 
2277
 
2278
  if not base64_image:
2279
  return jsonify({"error": "Изображение не найдено в запросе."}), 400
 
2280
  try:
2281
  image_data = base64.b64decode(base64_image)
2282
  result_text = generate_ai_description_from_image(image_data, language)
 
2294
 
2295
  if not user_message:
2296
  return jsonify({"error": "Сообщение не может быть пустым."}), 400
 
2297
  try:
2298
  ai_response_text = generate_chat_response(user_message, chat_history_from_client)
2299
  return jsonify({"text": ai_response_text})