Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
| 1 |
|
| 2 |
-
|
| 3 |
-
|
| 4 |
from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify
|
| 5 |
import json
|
| 6 |
import os
|
|
@@ -24,7 +22,7 @@ DATA_FILE = 'data.json'
|
|
| 24 |
|
| 25 |
SYNC_FILES = [DATA_FILE]
|
| 26 |
|
| 27 |
-
REPO_ID = "Kgshop/Mobilmir"
|
| 28 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
|
| 29 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 30 |
|
|
@@ -36,6 +34,13 @@ CURRENCY_NAME = 'Казахстанский тенге'
|
|
| 36 |
DOWNLOAD_RETRIES = 3
|
| 37 |
DOWNLOAD_DELAY = 5
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 40 |
|
| 41 |
|
|
@@ -1020,6 +1025,23 @@ ADMIN_TEMPLATE = '''
|
|
| 1020 |
.status-indicator.in-stock { background-color: #c6f6d5; color: #2f855a; }
|
| 1021 |
.status-indicator.out-of-stock { background-color: #fed7d7; color: #c53030; }
|
| 1022 |
.status-indicator.top-product { background-color: #feebc8; color: #9c4221; margin-left: 5px;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1023 |
</style>
|
| 1024 |
</head>
|
| 1025 |
<body>
|
|
@@ -1054,6 +1076,68 @@ ADMIN_TEMPLATE = '''
|
|
| 1054 |
<p style="font-size: 0.85rem; color: #a0aec0;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
|
| 1055 |
</div>
|
| 1056 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1057 |
<div class="flex-container">
|
| 1058 |
<div class="flex-item">
|
| 1059 |
<div class="section">
|
|
@@ -1431,6 +1515,7 @@ def admin():
|
|
| 1431 |
data = load_data()
|
| 1432 |
products = data.get('products', [])
|
| 1433 |
categories = data.get('categories', [])
|
|
|
|
| 1434 |
|
| 1435 |
needs_save = False
|
| 1436 |
for product in products:
|
|
@@ -1451,7 +1536,30 @@ def admin():
|
|
| 1451 |
logging.info(f"Admin action received: {action}")
|
| 1452 |
|
| 1453 |
try:
|
| 1454 |
-
if action == '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1455 |
category_name = request.form.get('category_name', '').strip()
|
| 1456 |
if category_name and category_name not in categories:
|
| 1457 |
categories.append(category_name)
|
|
@@ -1744,11 +1852,14 @@ def admin():
|
|
| 1744 |
current_data = load_data()
|
| 1745 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 1746 |
display_categories = sorted(current_data.get('categories', []))
|
|
|
|
| 1747 |
|
| 1748 |
return render_template_string(
|
| 1749 |
ADMIN_TEMPLATE,
|
| 1750 |
products=display_products,
|
| 1751 |
categories=display_categories,
|
|
|
|
|
|
|
| 1752 |
repo_id=REPO_ID,
|
| 1753 |
currency_code=CURRENCY_CODE
|
| 1754 |
)
|
|
|
|
| 1 |
|
|
|
|
|
|
|
| 2 |
from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify
|
| 3 |
import json
|
| 4 |
import os
|
|
|
|
| 22 |
|
| 23 |
SYNC_FILES = [DATA_FILE]
|
| 24 |
|
| 25 |
+
REPO_ID = "Kgshop/Mobilmir"
|
| 26 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
|
| 27 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 28 |
|
|
|
|
| 34 |
DOWNLOAD_RETRIES = 3
|
| 35 |
DOWNLOAD_DELAY = 5
|
| 36 |
|
| 37 |
+
STATUS_MAP_RU = {
|
| 38 |
+
"new": "Новый",
|
| 39 |
+
"accepted": "Принят",
|
| 40 |
+
"prepared": "Собран",
|
| 41 |
+
"shipped": "Отправлен"
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 45 |
|
| 46 |
|
|
|
|
| 1025 |
.status-indicator.in-stock { background-color: #c6f6d5; color: #2f855a; }
|
| 1026 |
.status-indicator.out-of-stock { background-color: #fed7d7; color: #c53030; }
|
| 1027 |
.status-indicator.top-product { background-color: #feebc8; color: #9c4221; margin-left: 5px;}
|
| 1028 |
+
|
| 1029 |
+
/* Order Specific Status Styles */
|
| 1030 |
+
.status-indicator.new { background-color: #fefcbf; color: #744210; }
|
| 1031 |
+
.status-indicator.accepted { background-color: #b3e5fc; color: #0277bd; }
|
| 1032 |
+
.status-indicator.prepared { background-color: #c6f6d5; color: #2f855a; }
|
| 1033 |
+
.status-indicator.shipped { background-color: #a7f3d0; color: #065f46; }
|
| 1034 |
+
|
| 1035 |
+
.order-details-content { padding: 10px 0 0 0; }
|
| 1036 |
+
.order-item-detail { display: flex; gap: 15px; padding: 8px 0; border-top: 1px dashed #E0E7FF; font-size: 0.9rem; align-items: center; }
|
| 1037 |
+
.order-item-detail:first-child { border-top: none; }
|
| 1038 |
+
.order-item-detail img { width: 40px; height: 40px; object-fit: contain; border-radius: 4px; }
|
| 1039 |
+
.order-item-name { flex-grow: 1; }
|
| 1040 |
+
.order-item-qty-price { width: 150px; text-align: right; flex-shrink: 0; }
|
| 1041 |
+
.order-total-summary { font-size: 1.1rem; font-weight: 600; margin-top: 15px; padding-top: 10px; border-top: 1px solid #4F46E5; text-align: right; }
|
| 1042 |
+
.order-status-form { display: flex; gap: 10px; align-items: center; margin-top: 15px; padding-top: 15px; border-top: 1px dashed #E0E7FF; flex-wrap: wrap; }
|
| 1043 |
+
.order-status-form select { max-width: 200px; margin-top: 0; flex-grow: 1;}
|
| 1044 |
+
.order-status-form .button { margin-top: 0; flex-grow: 0; }
|
| 1045 |
</style>
|
| 1046 |
</head>
|
| 1047 |
<body>
|
|
|
|
| 1076 |
<p style="font-size: 0.85rem; color: #a0aec0;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
|
| 1077 |
</div>
|
| 1078 |
|
| 1079 |
+
<div class="section">
|
| 1080 |
+
<h2><i class="fas fa-history"></i> История заказов (Предполагается Алматинское время)</h2>
|
| 1081 |
+
|
| 1082 |
+
{% if orders %}
|
| 1083 |
+
{% set sorted_orders = orders | sort(attribute='created_at', reverse=true) %}
|
| 1084 |
+
{% for order in sorted_orders %}
|
| 1085 |
+
{% set current_status = order.get('status', 'new') %}
|
| 1086 |
+
{% set total_items = order.cart | sum(attribute='quantity') %}
|
| 1087 |
+
<details style="margin-bottom: 10px;">
|
| 1088 |
+
<summary style="display: flex; justify-content: space-between; align-items: center; padding: 15px 20px;">
|
| 1089 |
+
<span>
|
| 1090 |
+
Заказ №{{ order.id }}
|
| 1091 |
+
<span class="status-indicator {{ current_status }}">{{ status_map_ru.get(current_status, current_status) }}</span>
|
| 1092 |
+
</span>
|
| 1093 |
+
<span style="font-size: 0.9rem; color: #4d333f; flex-shrink: 0;">
|
| 1094 |
+
Дата: {{ order.created_at }} | Итого: {{ "%.2f"|format(order.total_price) }} {{ currency_code }} ({{ total_items }} шт.)
|
| 1095 |
+
</span>
|
| 1096 |
+
</summary>
|
| 1097 |
+
<div class="form-content">
|
| 1098 |
+
<h3>Состав заказа:</h3>
|
| 1099 |
+
<div class="order-details-content">
|
| 1100 |
+
{% for item in order.cart %}
|
| 1101 |
+
{% set item_price = item.price * item.quantity %}
|
| 1102 |
+
<div class="order-item-detail">
|
| 1103 |
+
<img src="{{ item.photo_url }}" alt="{{ item.name }}">
|
| 1104 |
+
<div class="order-item-name">
|
| 1105 |
+
<strong>{{ item.name }}</strong>
|
| 1106 |
+
{% if item.color != 'N/A' %}<p style="margin: 0; color: #a0aec0;">Цвет: {{ item.color }}</p>{% endif %}
|
| 1107 |
+
</div>
|
| 1108 |
+
<div class="order-item-qty-price">
|
| 1109 |
+
{{ item.quantity }} × {{ "%.2f"|format(item.price) }} {{ currency_code }} = <strong>{{ "%.2f"|format(item_price) }} {{ currency_code }}</strong>
|
| 1110 |
+
</div>
|
| 1111 |
+
</div>
|
| 1112 |
+
{% endfor %}
|
| 1113 |
+
</div>
|
| 1114 |
+
<div class="order-total-summary">
|
| 1115 |
+
Общая сумма: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}
|
| 1116 |
+
</div>
|
| 1117 |
+
|
| 1118 |
+
<div class="order-status-form">
|
| 1119 |
+
<form method="POST" style="display: flex; gap: 10px; width: 100%; align-items: center; flex-wrap: wrap;">
|
| 1120 |
+
<input type="hidden" name="action" value="update_order_status">
|
| 1121 |
+
<input type="hidden" name="order_id" value="{{ order.id }}">
|
| 1122 |
+
<label for="status_{{ order.id }}" style="margin: 0; display: inline; font-size: 0.9rem;">Изменить статус:</label>
|
| 1123 |
+
<select name="new_status" id="status_{{ order.id }}" style="max-width: 200px; margin: 0;">
|
| 1124 |
+
{% for status_key, status_value in status_map_ru.items() %}
|
| 1125 |
+
<option value="{{ status_key }}" {% if current_status == status_key %}selected{% endif %}>{{ status_value }}</option>
|
| 1126 |
+
{% endfor %}
|
| 1127 |
+
</select>
|
| 1128 |
+
<button type="submit" class="button"><i class="fas fa-check"></i> Сохранить</button>
|
| 1129 |
+
<a href="{{ url_for('view_order', order_id=order.id) }}" target="_blank" class="button" style="background-color: #6366F1;"><i class="fas fa-eye"></i> Просмотр</a>
|
| 1130 |
+
</form>
|
| 1131 |
+
</div>
|
| 1132 |
+
</div>
|
| 1133 |
+
</details>
|
| 1134 |
+
{% endfor %}
|
| 1135 |
+
{% else %}
|
| 1136 |
+
<p>Активных заказов нет.</p>
|
| 1137 |
+
{% endif %}
|
| 1138 |
+
</div>
|
| 1139 |
+
|
| 1140 |
+
|
| 1141 |
<div class="flex-container">
|
| 1142 |
<div class="flex-item">
|
| 1143 |
<div class="section">
|
|
|
|
| 1515 |
data = load_data()
|
| 1516 |
products = data.get('products', [])
|
| 1517 |
categories = data.get('categories', [])
|
| 1518 |
+
orders = data.get('orders', {})
|
| 1519 |
|
| 1520 |
needs_save = False
|
| 1521 |
for product in products:
|
|
|
|
| 1536 |
logging.info(f"Admin action received: {action}")
|
| 1537 |
|
| 1538 |
try:
|
| 1539 |
+
if action == 'update_order_status':
|
| 1540 |
+
order_id = request.form.get('order_id')
|
| 1541 |
+
new_status = request.form.get('new_status')
|
| 1542 |
+
|
| 1543 |
+
if order_id in data['orders']:
|
| 1544 |
+
order = data['orders'][order_id]
|
| 1545 |
+
valid_statuses = list(STATUS_MAP_RU.keys())
|
| 1546 |
+
|
| 1547 |
+
if new_status in valid_statuses:
|
| 1548 |
+
old_status_key = order.get('status', 'new')
|
| 1549 |
+
|
| 1550 |
+
order['status'] = new_status
|
| 1551 |
+
data['orders'][order_id] = order
|
| 1552 |
+
save_data(data)
|
| 1553 |
+
logging.info(f"Order {order_id} status updated from {old_status_key} to {new_status}")
|
| 1554 |
+
flash(f"Статус заказа №{order_id} успешно изменен на '{STATUS_MAP_RU.get(new_status)}'.", 'success')
|
| 1555 |
+
else:
|
| 1556 |
+
flash(f"Неверный статус: {new_status}", 'error')
|
| 1557 |
+
else:
|
| 1558 |
+
flash(f"Заказ №{order_id} не найден.", 'error')
|
| 1559 |
+
|
| 1560 |
+
return redirect(url_for('admin'))
|
| 1561 |
+
|
| 1562 |
+
elif action == 'add_category':
|
| 1563 |
category_name = request.form.get('category_name', '').strip()
|
| 1564 |
if category_name and category_name not in categories:
|
| 1565 |
categories.append(category_name)
|
|
|
|
| 1852 |
current_data = load_data()
|
| 1853 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 1854 |
display_categories = sorted(current_data.get('categories', []))
|
| 1855 |
+
display_orders = list(current_data.get('orders', {}).values())
|
| 1856 |
|
| 1857 |
return render_template_string(
|
| 1858 |
ADMIN_TEMPLATE,
|
| 1859 |
products=display_products,
|
| 1860 |
categories=display_categories,
|
| 1861 |
+
orders=display_orders,
|
| 1862 |
+
status_map_ru=STATUS_MAP_RU,
|
| 1863 |
repo_id=REPO_ID,
|
| 1864 |
currency_code=CURRENCY_CODE
|
| 1865 |
)
|