Update app.py
Browse files
app.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
from flask import Flask, render_template_string, request, redirect, url_for
|
| 2 |
import json
|
| 3 |
import os
|
| 4 |
-
import logging
|
| 5 |
import threading
|
| 6 |
import time
|
| 7 |
import uuid
|
|
@@ -15,14 +14,28 @@ REPO_ID = "Kgshop/vahid"
|
|
| 15 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
|
| 16 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 17 |
|
| 18 |
-
|
| 19 |
|
| 20 |
def get_almaty_time():
|
| 21 |
return datetime.now(zoneinfo.ZoneInfo("Asia/Almaty")).strftime('%d.%m.%Y %H:%M:%S')
|
| 22 |
|
| 23 |
def load_data():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
try:
|
| 25 |
-
download_db_from_hf()
|
| 26 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 27 |
data = json.load(file)
|
| 28 |
if not isinstance(data, dict):
|
|
@@ -66,7 +79,7 @@ def save_data(data):
|
|
| 66 |
try:
|
| 67 |
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 68 |
json.dump(data, file, ensure_ascii=False, indent=4)
|
| 69 |
-
upload_db_to_hf()
|
| 70 |
except Exception:
|
| 71 |
pass
|
| 72 |
|
|
@@ -84,19 +97,6 @@ def upload_db_to_hf():
|
|
| 84 |
except Exception:
|
| 85 |
pass
|
| 86 |
|
| 87 |
-
def download_db_from_hf():
|
| 88 |
-
try:
|
| 89 |
-
hf_hub_download(
|
| 90 |
-
repo_id=REPO_ID,
|
| 91 |
-
filename=DATA_FILE,
|
| 92 |
-
repo_type="dataset",
|
| 93 |
-
token=HF_TOKEN_READ,
|
| 94 |
-
local_dir=".",
|
| 95 |
-
local_dir_use_symlinks=False
|
| 96 |
-
)
|
| 97 |
-
except Exception:
|
| 98 |
-
pass
|
| 99 |
-
|
| 100 |
def periodic_backup():
|
| 101 |
while True:
|
| 102 |
upload_db_to_hf()
|
|
@@ -231,35 +231,38 @@ MAIN_HTML = '''
|
|
| 231 |
}
|
| 232 |
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', sans-serif; -webkit-tap-highlight-color: transparent; }
|
| 233 |
body { background: var(--bg); color: var(--text-main); padding: 16px; padding-bottom: 80px; }
|
| 234 |
-
.container { max-width:
|
| 235 |
h1 { text-align: center; font-size: 1.5rem; font-weight: 700; margin-bottom: 24px; }
|
| 236 |
|
| 237 |
.top-actions { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
| 238 |
-
.btn { flex: 1; padding: 12px; border: none; border-radius: 12px; font-size: 0.95rem; font-weight: 600; color: white; cursor: pointer; display: flex; justify-content: center; align-items: center; gap: 8px; }
|
|
|
|
| 239 |
.btn-primary { background: var(--primary); }
|
| 240 |
.btn-success { background: var(--success); }
|
| 241 |
.btn-danger { background: var(--danger); }
|
| 242 |
|
| 243 |
.main-nav { display: flex; gap: 8px; margin-bottom: 16px; overflow-x: auto; padding-bottom: 4px; }
|
| 244 |
-
.nav-btn { flex: 0 0 auto; padding: 10px 16px; border-radius: 8px; border: none; background: #e5e7eb; color: var(--text-muted); font-weight: 600; font-size: 0.9rem; cursor: pointer; }
|
| 245 |
.nav-btn.active { background: var(--primary); color: white; }
|
| 246 |
|
| 247 |
.loc-nav { display: flex; gap: 8px; margin-bottom: 16px; overflow-x: auto; padding-bottom: 4px; }
|
| 248 |
-
.loc-btn { flex: 0 0 auto; padding: 8px 14px; border-radius: 20px; border: 1px solid var(--border); background: var(--surface); color: var(--text-main); font-weight: 500; font-size: 0.85rem; cursor: pointer; }
|
| 249 |
.loc-btn.active { background: var(--text-main); color: white; border-color: var(--text-main); }
|
| 250 |
|
| 251 |
-
.search-box { width: 100%; padding: 14px; border: 1px solid var(--border); border-radius: 12px; outline: none; font-size: 1rem; margin-bottom: 16px; }
|
|
|
|
| 252 |
|
| 253 |
#catalog { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; }
|
| 254 |
.product { background: var(--surface); border-radius: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); display: flex; flex-direction: column; overflow: hidden; }
|
| 255 |
-
.product-img-wrapper { width: 100%; height: 160px; background: #f9fafb; }
|
| 256 |
.product-img { width: 100%; height: 100%; object-fit: cover; }
|
| 257 |
.product-info { padding: 12px; display: flex; flex-direction: column; flex: 1; }
|
| 258 |
.product-title { font-size: 1rem; font-weight: 600; margin-bottom: 4px; line-height: 1.2; }
|
| 259 |
.product-price { font-size: 1.1rem; font-weight: 700; color: var(--primary); margin-bottom: 4px; }
|
| 260 |
.product-stock { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 12px; }
|
| 261 |
.product-actions { display: flex; gap: 8px; align-items: center; margin-top: auto; }
|
| 262 |
-
.qty-input { width: 60px; padding: 8px; border: 1px solid var(--border); border-radius: 8px; text-align: center; font-weight: 600; outline: none; }
|
|
|
|
| 263 |
|
| 264 |
.history-item { background: var(--surface); border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
|
| 265 |
.history-header { display: flex; justify-content: space-between; font-size: 0.85rem; color: var(--text-muted); margin-bottom: 8px; }
|
|
@@ -272,19 +275,34 @@ MAIN_HTML = '''
|
|
| 272 |
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(17, 24, 39, 0.6); z-index: 1000; justify-content: center; align-items: flex-end; }
|
| 273 |
.modal-overlay.active { display: flex; }
|
| 274 |
.modal-content { background: var(--surface); padding: 24px; border-radius: 20px 20px 0 0; width: 100%; max-width: 600px; max-height: 90vh; overflow-y: auto; position: relative; }
|
| 275 |
-
@media (min-width: 600px) { .modal-overlay { align-items: center; } .modal-content { border-radius: 20px; } }
|
| 276 |
|
| 277 |
.close-modal { position: absolute; top: 20px; right: 20px; width: 32px; height: 32px; background: var(--bg); border-radius: 16px; display: flex; justify-content: center; align-items: center; cursor: pointer; border: none; font-size: 18px; }
|
| 278 |
-
.form-control { width: 100%; padding: 14px; border: 1px solid var(--border); border-radius: 12px; font-size: 1rem; outline: none; margin-bottom: 16px; }
|
|
|
|
| 279 |
|
| 280 |
-
.settings-
|
| 281 |
-
.settings-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
|
| 283 |
-
.cart-float { display: none; position: fixed; bottom: 24px; right: 24px; background: var(--text-main); color: white; padding: 14px 24px; border-radius: 30px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); cursor: pointer; z-index: 100; font-weight: 600; align-items: center; gap: 8px; }
|
|
|
|
| 284 |
|
| 285 |
.invoice-table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
| 286 |
.invoice-table th, .invoice-table td { padding: 10px; border-bottom: 1px solid var(--border); text-align: left; }
|
| 287 |
.invoice-total { font-size: 1.2rem; font-weight: 700; text-align: right; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
</style>
|
| 289 |
</head>
|
| 290 |
<body>
|
|
@@ -309,7 +327,7 @@ MAIN_HTML = '''
|
|
| 309 |
|
| 310 |
<div id="section_catalog">
|
| 311 |
<div class="loc-nav" id="loc_nav_container"></div>
|
| 312 |
-
<input type="text" id="search" class="search-box" placeholder="Поиск товара..." oninput="
|
| 313 |
<div id="catalog"></div>
|
| 314 |
</div>
|
| 315 |
|
|
@@ -324,34 +342,36 @@ MAIN_HTML = '''
|
|
| 324 |
</div>
|
| 325 |
|
| 326 |
<div id="section_settings" style="display:none;">
|
| 327 |
-
<div class="settings-
|
| 328 |
-
<
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
<
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
<
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
<
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
<
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
<
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
|
|
|
|
|
|
| 355 |
</div>
|
| 356 |
</div>
|
| 357 |
</div>
|
|
@@ -364,12 +384,12 @@ MAIN_HTML = '''
|
|
| 364 |
<div class="modal-content">
|
| 365 |
<button class="close-modal" onclick="closeModal('addModal')">×</button>
|
| 366 |
<h2>Добавить товар</h2>
|
| 367 |
-
<form method="POST">
|
| 368 |
<input type="hidden" name="action" value="add">
|
| 369 |
<input type="text" name="name" class="form-control" placeholder="Название" required>
|
| 370 |
<textarea name="description" class="form-control" placeholder="Описание"></textarea>
|
| 371 |
<input type="number" name="price" class="form-control" placeholder="Цена (₸)" required>
|
| 372 |
-
<label style="display:block; margin-bottom:8px; font-size:0.9rem; color:var(--text-muted);">Фото товара</label>
|
| 373 |
<input type="file" accept="image/*" class="form-control" onchange="processImage(this)">
|
| 374 |
<input type="hidden" name="photo" id="photo_b64">
|
| 375 |
<img id="photo_preview" style="display:none; width:100%; height:200px; object-fit:cover; border-radius:12px; margin-bottom:16px;">
|
|
@@ -384,7 +404,7 @@ MAIN_HTML = '''
|
|
| 384 |
<div class="modal-content">
|
| 385 |
<button class="close-modal" onclick="closeModal('incomeModal')">×</button>
|
| 386 |
<h2>Приход</h2>
|
| 387 |
-
<form method="POST">
|
| 388 |
<input type="hidden" name="action" value="income">
|
| 389 |
<select name="loc_id" id="inc_loc_select" class="form-control" required></select>
|
| 390 |
<select name="product_index" id="inc_product" class="form-control" required></select>
|
|
@@ -398,7 +418,7 @@ MAIN_HTML = '''
|
|
| 398 |
<div class="modal-content">
|
| 399 |
<button class="close-modal" onclick="closeModal('transferModal')">×</button>
|
| 400 |
<h2>Перемещение</h2>
|
| 401 |
-
<form method="POST">
|
| 402 |
<input type="hidden" name="action" value="transfer">
|
| 403 |
<input type="hidden" name="pIndex" id="trans_pIndex">
|
| 404 |
<input type="hidden" name="from_loc" id="trans_from">
|
|
@@ -414,7 +434,7 @@ MAIN_HTML = '''
|
|
| 414 |
<div class="modal-content">
|
| 415 |
<button class="close-modal" onclick="closeModal('cartModal')">×</button>
|
| 416 |
<h2>Корзина</h2>
|
| 417 |
-
<div id="cart_items" style="margin-bottom:16px;"></div>
|
| 418 |
<button class="btn btn-success" style="width:100%;" onclick="submitCart()">Оформить накладную</button>
|
| 419 |
</div>
|
| 420 |
</div>
|
|
@@ -439,6 +459,7 @@ MAIN_HTML = '''
|
|
| 439 |
const hostUrl = "{{ request.host_url }}";
|
| 440 |
let activeLoc = 'all';
|
| 441 |
let cart = [];
|
|
|
|
| 442 |
|
| 443 |
function init() {
|
| 444 |
if (role === 'admin' || role === 'employee') {
|
|
@@ -490,10 +511,15 @@ MAIN_HTML = '''
|
|
| 490 |
return found ? found.name : locId;
|
| 491 |
}
|
| 492 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
function renderCatalog() {
|
| 494 |
const q = document.getElementById('search').value.toLowerCase();
|
| 495 |
const cat = document.getElementById('catalog');
|
| 496 |
-
|
| 497 |
|
| 498 |
data.products.forEach((p, pIdx) => {
|
| 499 |
let match = p.name.toLowerCase().includes(q) || (p.description && p.description.toLowerCase().includes(q));
|
|
@@ -506,7 +532,7 @@ MAIN_HTML = '''
|
|
| 506 |
stock = (p.stocks || {})[activeLoc] || 0;
|
| 507 |
}
|
| 508 |
|
| 509 |
-
let imgHtml = p.photo ? `<div class="product-img-wrapper"><img src="${p.photo}" class="product-img"></div>` : `<div class="product-img-wrapper"
|
| 510 |
|
| 511 |
let html = `<div class="product">
|
| 512 |
${imgHtml}
|
|
@@ -539,8 +565,9 @@ MAIN_HTML = '''
|
|
| 539 |
}
|
| 540 |
|
| 541 |
html += `</div></div>`;
|
| 542 |
-
|
| 543 |
});
|
|
|
|
| 544 |
}
|
| 545 |
|
| 546 |
function addToCart(pIdx) {
|
|
@@ -582,7 +609,7 @@ MAIN_HTML = '''
|
|
| 582 |
</div>
|
| 583 |
<div style="display:flex; align-items:center; gap:12px;">
|
| 584 |
<span style="font-weight:700;">${sum} ₸</span>
|
| 585 |
-
<button style="border:none; background:var(--danger); color:white; border-radius:6px; padding:
|
| 586 |
</div>
|
| 587 |
</div>`;
|
| 588 |
});
|
|
@@ -688,13 +715,28 @@ MAIN_HTML = '''
|
|
| 688 |
|
| 689 |
function buildSettings() {
|
| 690 |
const wCont = document.getElementById('set_warehouses');
|
| 691 |
-
wCont.innerHTML = data.settings.warehouses.map(w => `<div class="settings-list-item"><span>${w.name}</span><form method="POST"><input type="hidden" name="action" value="del_loc"><input type="hidden" name="l_type" value="warehouses"><input type="hidden" name="l_id" value="${w.id}"><button type="submit"
|
| 692 |
|
| 693 |
const sCont = document.getElementById('set_shops');
|
| 694 |
-
sCont.innerHTML = data.settings.shops.map(s => `<div class="settings-list-item"><span>${s.name}</span><form method="POST"><input type="hidden" name="action" value="del_loc"><input type="hidden" name="l_type" value="shops"><input type="hidden" name="l_id" value="${s.id}"><button type="submit"
|
| 695 |
|
| 696 |
const eCont = document.getElementById('set_emps');
|
| 697 |
-
eCont.innerHTML = data.settings.employees.map(e => `
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 698 |
}
|
| 699 |
|
| 700 |
function buildLocSelects() {
|
|
@@ -723,7 +765,7 @@ MAIN_HTML = '''
|
|
| 723 |
|
| 724 |
function renderHistory() {
|
| 725 |
const hc = document.getElementById('history_container');
|
| 726 |
-
|
| 727 |
|
| 728 |
const fDate = new Date(document.getElementById('hist_from').value + "T00:00:00" || 0);
|
| 729 |
const tDate = new Date(document.getElementById('hist_to').value + "T23:59:59" || "2099-01-01T23:59:59");
|
|
@@ -740,7 +782,7 @@ MAIN_HTML = '''
|
|
| 740 |
let bCls = h.action.includes('Приход') ? 'badge-green' : 'badge-orange';
|
| 741 |
let locInfo = h.action === 'Перемещен��е' ? `${h.from_loc_name} ➔ ${h.to_loc_name}` : (h.from_loc_name || h.to_loc_name);
|
| 742 |
|
| 743 |
-
|
| 744 |
<div class="history-header">
|
| 745 |
<span>${h.time} | ${h.emp_name}</span>
|
| 746 |
<span class="history-badge ${bCls}">${h.action}</span>
|
|
@@ -752,6 +794,7 @@ MAIN_HTML = '''
|
|
| 752 |
</div>`);
|
| 753 |
}
|
| 754 |
});
|
|
|
|
| 755 |
document.getElementById('total_sales').innerText = `Итого продаж: ${total} ₸`;
|
| 756 |
}
|
| 757 |
|
|
@@ -788,10 +831,7 @@ def employee_panel(token):
|
|
| 788 |
return render_template_string(MAIN_HTML, data=data, role='employee', emp=emp)
|
| 789 |
|
| 790 |
if __name__ == '__main__':
|
|
|
|
| 791 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 792 |
backup_thread.start()
|
| 793 |
-
try:
|
| 794 |
-
load_data()
|
| 795 |
-
except Exception:
|
| 796 |
-
pass
|
| 797 |
app.run(debug=True, host='0.0.0.0', port=7860)
|
|
|
|
| 1 |
from flask import Flask, render_template_string, request, redirect, url_for
|
| 2 |
import json
|
| 3 |
import os
|
|
|
|
| 4 |
import threading
|
| 5 |
import time
|
| 6 |
import uuid
|
|
|
|
| 14 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
|
| 15 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 16 |
|
| 17 |
+
INITIALIZED = False
|
| 18 |
|
| 19 |
def get_almaty_time():
|
| 20 |
return datetime.now(zoneinfo.ZoneInfo("Asia/Almaty")).strftime('%d.%m.%Y %H:%M:%S')
|
| 21 |
|
| 22 |
def load_data():
|
| 23 |
+
global INITIALIZED
|
| 24 |
+
if not INITIALIZED:
|
| 25 |
+
try:
|
| 26 |
+
hf_hub_download(
|
| 27 |
+
repo_id=REPO_ID,
|
| 28 |
+
filename=DATA_FILE,
|
| 29 |
+
repo_type="dataset",
|
| 30 |
+
token=HF_TOKEN_READ,
|
| 31 |
+
local_dir=".",
|
| 32 |
+
local_dir_use_symlinks=False
|
| 33 |
+
)
|
| 34 |
+
except Exception:
|
| 35 |
+
pass
|
| 36 |
+
INITIALIZED = True
|
| 37 |
+
|
| 38 |
try:
|
|
|
|
| 39 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 40 |
data = json.load(file)
|
| 41 |
if not isinstance(data, dict):
|
|
|
|
| 79 |
try:
|
| 80 |
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 81 |
json.dump(data, file, ensure_ascii=False, indent=4)
|
| 82 |
+
threading.Thread(target=upload_db_to_hf, daemon=True).start()
|
| 83 |
except Exception:
|
| 84 |
pass
|
| 85 |
|
|
|
|
| 97 |
except Exception:
|
| 98 |
pass
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
def periodic_backup():
|
| 101 |
while True:
|
| 102 |
upload_db_to_hf()
|
|
|
|
| 231 |
}
|
| 232 |
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', sans-serif; -webkit-tap-highlight-color: transparent; }
|
| 233 |
body { background: var(--bg); color: var(--text-main); padding: 16px; padding-bottom: 80px; }
|
| 234 |
+
.container { max-width: 1200px; margin: 0 auto; }
|
| 235 |
h1 { text-align: center; font-size: 1.5rem; font-weight: 700; margin-bottom: 24px; }
|
| 236 |
|
| 237 |
.top-actions { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
| 238 |
+
.btn { flex: 1; padding: 12px; border: none; border-radius: 12px; font-size: 0.95rem; font-weight: 600; color: white; cursor: pointer; display: flex; justify-content: center; align-items: center; gap: 8px; transition: transform 0.1s, opacity 0.2s; }
|
| 239 |
+
.btn:active { transform: scale(0.98); }
|
| 240 |
.btn-primary { background: var(--primary); }
|
| 241 |
.btn-success { background: var(--success); }
|
| 242 |
.btn-danger { background: var(--danger); }
|
| 243 |
|
| 244 |
.main-nav { display: flex; gap: 8px; margin-bottom: 16px; overflow-x: auto; padding-bottom: 4px; }
|
| 245 |
+
.nav-btn { flex: 0 0 auto; padding: 10px 16px; border-radius: 8px; border: none; background: #e5e7eb; color: var(--text-muted); font-weight: 600; font-size: 0.9rem; cursor: pointer; transition: background 0.2s, color 0.2s; }
|
| 246 |
.nav-btn.active { background: var(--primary); color: white; }
|
| 247 |
|
| 248 |
.loc-nav { display: flex; gap: 8px; margin-bottom: 16px; overflow-x: auto; padding-bottom: 4px; }
|
| 249 |
+
.loc-btn { flex: 0 0 auto; padding: 8px 14px; border-radius: 20px; border: 1px solid var(--border); background: var(--surface); color: var(--text-main); font-weight: 500; font-size: 0.85rem; cursor: pointer; transition: background 0.2s, color 0.2s; }
|
| 250 |
.loc-btn.active { background: var(--text-main); color: white; border-color: var(--text-main); }
|
| 251 |
|
| 252 |
+
.search-box { width: 100%; padding: 14px; border: 1px solid var(--border); border-radius: 12px; outline: none; font-size: 1rem; margin-bottom: 16px; transition: border-color 0.2s; }
|
| 253 |
+
.search-box:focus { border-color: var(--primary); }
|
| 254 |
|
| 255 |
#catalog { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; }
|
| 256 |
.product { background: var(--surface); border-radius: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); display: flex; flex-direction: column; overflow: hidden; }
|
| 257 |
+
.product-img-wrapper { width: 100%; height: 160px; background: #f9fafb; display: flex; align-items: center; justify-content: center; color: #9ca3af; }
|
| 258 |
.product-img { width: 100%; height: 100%; object-fit: cover; }
|
| 259 |
.product-info { padding: 12px; display: flex; flex-direction: column; flex: 1; }
|
| 260 |
.product-title { font-size: 1rem; font-weight: 600; margin-bottom: 4px; line-height: 1.2; }
|
| 261 |
.product-price { font-size: 1.1rem; font-weight: 700; color: var(--primary); margin-bottom: 4px; }
|
| 262 |
.product-stock { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 12px; }
|
| 263 |
.product-actions { display: flex; gap: 8px; align-items: center; margin-top: auto; }
|
| 264 |
+
.qty-input { width: 60px; padding: 8px; border: 1px solid var(--border); border-radius: 8px; text-align: center; font-weight: 600; outline: none; transition: border-color 0.2s; }
|
| 265 |
+
.qty-input:focus { border-color: var(--primary); }
|
| 266 |
|
| 267 |
.history-item { background: var(--surface); border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
|
| 268 |
.history-header { display: flex; justify-content: space-between; font-size: 0.85rem; color: var(--text-muted); margin-bottom: 8px; }
|
|
|
|
| 275 |
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(17, 24, 39, 0.6); z-index: 1000; justify-content: center; align-items: flex-end; }
|
| 276 |
.modal-overlay.active { display: flex; }
|
| 277 |
.modal-content { background: var(--surface); padding: 24px; border-radius: 20px 20px 0 0; width: 100%; max-width: 600px; max-height: 90vh; overflow-y: auto; position: relative; }
|
|
|
|
| 278 |
|
| 279 |
.close-modal { position: absolute; top: 20px; right: 20px; width: 32px; height: 32px; background: var(--bg); border-radius: 16px; display: flex; justify-content: center; align-items: center; cursor: pointer; border: none; font-size: 18px; }
|
| 280 |
+
.form-control { width: 100%; padding: 14px; border: 1px solid var(--border); border-radius: 12px; font-size: 1rem; outline: none; margin-bottom: 16px; transition: border-color 0.2s; }
|
| 281 |
+
.form-control:focus { border-color: var(--primary); }
|
| 282 |
|
| 283 |
+
.settings-grid { display: grid; gap: 16px; grid-template-columns: 1fr; }
|
| 284 |
+
.settings-card { background: var(--surface); border-radius: 12px; padding: 16px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
|
| 285 |
+
.settings-list-item { display: flex; flex-direction: column; padding: 12px 0; border-bottom: 1px solid var(--border); gap: 12px; }
|
| 286 |
+
.settings-list-item:last-child { border-bottom: none; }
|
| 287 |
+
.settings-list-item-header { display: flex; justify-content: space-between; align-items: center; }
|
| 288 |
+
.settings-link-box { display: flex; gap: 8px; align-items: center; width: 100%; }
|
| 289 |
+
.settings-link-input { flex: 1; padding: 10px; border: 1px solid var(--border); border-radius: 8px; font-size: 0.85rem; background: var(--bg); outline: none; color: var(--text-main); }
|
| 290 |
|
| 291 |
+
.cart-float { display: none; position: fixed; bottom: 24px; right: 24px; background: var(--text-main); color: white; padding: 14px 24px; border-radius: 30px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); cursor: pointer; z-index: 100; font-weight: 600; align-items: center; gap: 8px; transition: transform 0.2s; }
|
| 292 |
+
.cart-float:active { transform: scale(0.95); }
|
| 293 |
|
| 294 |
.invoice-table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
| 295 |
.invoice-table th, .invoice-table td { padding: 10px; border-bottom: 1px solid var(--border); text-align: left; }
|
| 296 |
.invoice-total { font-size: 1.2rem; font-weight: 700; text-align: right; }
|
| 297 |
+
|
| 298 |
+
@media (min-width: 768px) {
|
| 299 |
+
body { padding: 24px; }
|
| 300 |
+
.top-actions { max-width: 400px; }
|
| 301 |
+
#catalog { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
|
| 302 |
+
.settings-grid { grid-template-columns: repeat(3, 1fr); gap: 24px; }
|
| 303 |
+
.modal-overlay { align-items: center; }
|
| 304 |
+
.modal-content { border-radius: 20px; max-width: 500px; }
|
| 305 |
+
}
|
| 306 |
</style>
|
| 307 |
</head>
|
| 308 |
<body>
|
|
|
|
| 327 |
|
| 328 |
<div id="section_catalog">
|
| 329 |
<div class="loc-nav" id="loc_nav_container"></div>
|
| 330 |
+
<input type="text" id="search" class="search-box" placeholder="Поиск товара..." oninput="onSearchInput()">
|
| 331 |
<div id="catalog"></div>
|
| 332 |
</div>
|
| 333 |
|
|
|
|
| 342 |
</div>
|
| 343 |
|
| 344 |
<div id="section_settings" style="display:none;">
|
| 345 |
+
<div class="settings-grid">
|
| 346 |
+
<div class="settings-card">
|
| 347 |
+
<h3>Склады</h3>
|
| 348 |
+
<div id="set_warehouses"></div>
|
| 349 |
+
<form method="POST" style="display:flex; gap:8px; margin-top:12px;">
|
| 350 |
+
<input type="hidden" name="action" value="add_loc">
|
| 351 |
+
<input type="hidden" name="l_type" value="warehouses">
|
| 352 |
+
<input type="text" name="l_name" class="form-control" style="margin:0;" placeholder="Название" required>
|
| 353 |
+
<button type="submit" class="btn btn-primary" style="flex:0 0 auto; padding:0 20px;">+</button>
|
| 354 |
+
</form>
|
| 355 |
+
</div>
|
| 356 |
+
<div class="settings-card">
|
| 357 |
+
<h3>Магазины</h3>
|
| 358 |
+
<div id="set_shops"></div>
|
| 359 |
+
<form method="POST" style="display:flex; gap:8px; margin-top:12px;">
|
| 360 |
+
<input type="hidden" name="action" value="add_loc">
|
| 361 |
+
<input type="hidden" name="l_type" value="shops">
|
| 362 |
+
<input type="text" name="l_name" class="form-control" style="margin:0;" placeholder="Название" required>
|
| 363 |
+
<button type="submit" class="btn btn-primary" style="flex:0 0 auto; padding:0 20px;">+</button>
|
| 364 |
+
</form>
|
| 365 |
+
</div>
|
| 366 |
+
<div class="settings-card">
|
| 367 |
+
<h3>Сотрудники</h3>
|
| 368 |
+
<div id="set_emps"></div>
|
| 369 |
+
<form method="POST" style="display:flex; gap:8px; margin-top:12px;">
|
| 370 |
+
<input type="hidden" name="action" value="add_emp">
|
| 371 |
+
<input type="text" name="e_name" class="form-control" style="margin:0;" placeholder="Имя сотрудника" required>
|
| 372 |
+
<button type="submit" class="btn btn-primary" style="flex:0 0 auto; padding:0 20px;">+</button>
|
| 373 |
+
</form>
|
| 374 |
+
</div>
|
| 375 |
</div>
|
| 376 |
</div>
|
| 377 |
</div>
|
|
|
|
| 384 |
<div class="modal-content">
|
| 385 |
<button class="close-modal" onclick="closeModal('addModal')">×</button>
|
| 386 |
<h2>Добавить товар</h2>
|
| 387 |
+
<form method="POST" style="margin-top:16px;">
|
| 388 |
<input type="hidden" name="action" value="add">
|
| 389 |
<input type="text" name="name" class="form-control" placeholder="Название" required>
|
| 390 |
<textarea name="description" class="form-control" placeholder="Описание"></textarea>
|
| 391 |
<input type="number" name="price" class="form-control" placeholder="Цена (₸)" required>
|
| 392 |
+
<label style="display:block; margin-bottom:8px; font-size:0.9rem; color:var(--text-muted);">Фото товара (не обязательно)</label>
|
| 393 |
<input type="file" accept="image/*" class="form-control" onchange="processImage(this)">
|
| 394 |
<input type="hidden" name="photo" id="photo_b64">
|
| 395 |
<img id="photo_preview" style="display:none; width:100%; height:200px; object-fit:cover; border-radius:12px; margin-bottom:16px;">
|
|
|
|
| 404 |
<div class="modal-content">
|
| 405 |
<button class="close-modal" onclick="closeModal('incomeModal')">×</button>
|
| 406 |
<h2>Приход</h2>
|
| 407 |
+
<form method="POST" style="margin-top:16px;">
|
| 408 |
<input type="hidden" name="action" value="income">
|
| 409 |
<select name="loc_id" id="inc_loc_select" class="form-control" required></select>
|
| 410 |
<select name="product_index" id="inc_product" class="form-control" required></select>
|
|
|
|
| 418 |
<div class="modal-content">
|
| 419 |
<button class="close-modal" onclick="closeModal('transferModal')">×</button>
|
| 420 |
<h2>Перемещение</h2>
|
| 421 |
+
<form method="POST" style="margin-top:16px;">
|
| 422 |
<input type="hidden" name="action" value="transfer">
|
| 423 |
<input type="hidden" name="pIndex" id="trans_pIndex">
|
| 424 |
<input type="hidden" name="from_loc" id="trans_from">
|
|
|
|
| 434 |
<div class="modal-content">
|
| 435 |
<button class="close-modal" onclick="closeModal('cartModal')">×</button>
|
| 436 |
<h2>Корзина</h2>
|
| 437 |
+
<div id="cart_items" style="margin-bottom:16px; margin-top:16px;"></div>
|
| 438 |
<button class="btn btn-success" style="width:100%;" onclick="submitCart()">Оформить накладную</button>
|
| 439 |
</div>
|
| 440 |
</div>
|
|
|
|
| 459 |
const hostUrl = "{{ request.host_url }}";
|
| 460 |
let activeLoc = 'all';
|
| 461 |
let cart = [];
|
| 462 |
+
let searchTimeout;
|
| 463 |
|
| 464 |
function init() {
|
| 465 |
if (role === 'admin' || role === 'employee') {
|
|
|
|
| 511 |
return found ? found.name : locId;
|
| 512 |
}
|
| 513 |
|
| 514 |
+
function onSearchInput() {
|
| 515 |
+
clearTimeout(searchTimeout);
|
| 516 |
+
searchTimeout = setTimeout(renderCatalog, 200);
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
function renderCatalog() {
|
| 520 |
const q = document.getElementById('search').value.toLowerCase();
|
| 521 |
const cat = document.getElementById('catalog');
|
| 522 |
+
let htmlArr = [];
|
| 523 |
|
| 524 |
data.products.forEach((p, pIdx) => {
|
| 525 |
let match = p.name.toLowerCase().includes(q) || (p.description && p.description.toLowerCase().includes(q));
|
|
|
|
| 532 |
stock = (p.stocks || {})[activeLoc] || 0;
|
| 533 |
}
|
| 534 |
|
| 535 |
+
let imgHtml = p.photo ? `<div class="product-img-wrapper"><img src="${p.photo}" class="product-img"></div>` : `<div class="product-img-wrapper">Нет фото</div>`;
|
| 536 |
|
| 537 |
let html = `<div class="product">
|
| 538 |
${imgHtml}
|
|
|
|
| 565 |
}
|
| 566 |
|
| 567 |
html += `</div></div>`;
|
| 568 |
+
htmlArr.push(html);
|
| 569 |
});
|
| 570 |
+
cat.innerHTML = htmlArr.join('');
|
| 571 |
}
|
| 572 |
|
| 573 |
function addToCart(pIdx) {
|
|
|
|
| 609 |
</div>
|
| 610 |
<div style="display:flex; align-items:center; gap:12px;">
|
| 611 |
<span style="font-weight:700;">${sum} ₸</span>
|
| 612 |
+
<button style="border:none; background:var(--danger); color:white; border-radius:6px; padding:6px 10px; cursor:pointer; font-weight:700;" onclick="removeFromCart(${i})">×</button>
|
| 613 |
</div>
|
| 614 |
</div>`;
|
| 615 |
});
|
|
|
|
| 715 |
|
| 716 |
function buildSettings() {
|
| 717 |
const wCont = document.getElementById('set_warehouses');
|
| 718 |
+
wCont.innerHTML = data.settings.warehouses.map(w => `<div class="settings-list-item"><div class="settings-list-item-header"><span style="font-weight:600;">${w.name}</span><form method="POST" style="margin:0;"><input type="hidden" name="action" value="del_loc"><input type="hidden" name="l_type" value="warehouses"><input type="hidden" name="l_id" value="${w.id}"><button type="submit" class="btn btn-danger" style="padding:6px 12px; font-size:0.85rem;">Удалить</button></form></div></div>`).join('');
|
| 719 |
|
| 720 |
const sCont = document.getElementById('set_shops');
|
| 721 |
+
sCont.innerHTML = data.settings.shops.map(s => `<div class="settings-list-item"><div class="settings-list-item-header"><span style="font-weight:600;">${s.name}</span><form method="POST" style="margin:0;"><input type="hidden" name="action" value="del_loc"><input type="hidden" name="l_type" value="shops"><input type="hidden" name="l_id" value="${s.id}"><button type="submit" class="btn btn-danger" style="padding:6px 12px; font-size:0.85rem;">Удалить</button></form></div></div>`).join('');
|
| 722 |
|
| 723 |
const eCont = document.getElementById('set_emps');
|
| 724 |
+
eCont.innerHTML = data.settings.employees.map(e => `
|
| 725 |
+
<div class="settings-list-item">
|
| 726 |
+
<div class="settings-list-item-header">
|
| 727 |
+
<span style="font-weight:600;">${e.name}</span>
|
| 728 |
+
<form method="POST" style="margin:0;">
|
| 729 |
+
<input type="hidden" name="action" value="del_emp">
|
| 730 |
+
<input type="hidden" name="e_id" value="${e.id}">
|
| 731 |
+
<button type="submit" class="btn btn-danger" style="padding:6px 12px; font-size:0.85rem;">Удалить</button>
|
| 732 |
+
</form>
|
| 733 |
+
</div>
|
| 734 |
+
<div class="settings-link-box">
|
| 735 |
+
<input type="text" class="settings-link-input" value="${hostUrl}emp/${e.token}" readonly onclick="this.select()">
|
| 736 |
+
<button type="button" onclick="navigator.clipboard.writeText('${hostUrl}emp/${e.token}');alert('Ссылка скопирована')" class="btn btn-primary" style="padding:8px 12px; font-size:0.85rem; flex:0 0 auto;">Копировать</button>
|
| 737 |
+
</div>
|
| 738 |
+
</div>
|
| 739 |
+
`).join('');
|
| 740 |
}
|
| 741 |
|
| 742 |
function buildLocSelects() {
|
|
|
|
| 765 |
|
| 766 |
function renderHistory() {
|
| 767 |
const hc = document.getElementById('history_container');
|
| 768 |
+
let htmlArr = [];
|
| 769 |
|
| 770 |
const fDate = new Date(document.getElementById('hist_from').value + "T00:00:00" || 0);
|
| 771 |
const tDate = new Date(document.getElementById('hist_to').value + "T23:59:59" || "2099-01-01T23:59:59");
|
|
|
|
| 782 |
let bCls = h.action.includes('Приход') ? 'badge-green' : 'badge-orange';
|
| 783 |
let locInfo = h.action === 'Перемещен��е' ? `${h.from_loc_name} ➔ ${h.to_loc_name}` : (h.from_loc_name || h.to_loc_name);
|
| 784 |
|
| 785 |
+
htmlArr.push(`<div class="history-item">
|
| 786 |
<div class="history-header">
|
| 787 |
<span>${h.time} | ${h.emp_name}</span>
|
| 788 |
<span class="history-badge ${bCls}">${h.action}</span>
|
|
|
|
| 794 |
</div>`);
|
| 795 |
}
|
| 796 |
});
|
| 797 |
+
hc.innerHTML = htmlArr.join('');
|
| 798 |
document.getElementById('total_sales').innerText = `Итого продаж: ${total} ₸`;
|
| 799 |
}
|
| 800 |
|
|
|
|
| 831 |
return render_template_string(MAIN_HTML, data=data, role='employee', emp=emp)
|
| 832 |
|
| 833 |
if __name__ == '__main__':
|
| 834 |
+
load_data()
|
| 835 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 836 |
backup_thread.start()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
app.run(debug=True, host='0.0.0.0', port=7860)
|