Kgshop commited on
Commit
66efd2c
·
verified ·
1 Parent(s): 038ffd0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +116 -76
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
- logging.basicConfig(level=logging.ERROR)
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: 800px; 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; }
 
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-card { background: var(--surface); border-radius: 12px; padding: 16px; margin-bottom: 16px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
281
- .settings-list-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px solid var(--border); }
 
 
 
 
 
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="renderCatalog()">
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-card">
328
- <h3>Склады</h3>
329
- <div id="set_warehouses"></div>
330
- <form method="POST" style="display:flex; gap:8px; margin-top:12px;">
331
- <input type="hidden" name="action" value="add_loc">
332
- <input type="hidden" name="l_type" value="warehouses">
333
- <input type="text" name="l_name" class="form-control" style="margin:0;" placeholder="Название" required>
334
- <button type="submit" class="btn btn-primary" style="flex:0 0 auto; padding:0 20px;">+</button>
335
- </form>
336
- </div>
337
- <div class="settings-card">
338
- <h3>Магазины</h3>
339
- <div id="set_shops"></div>
340
- <form method="POST" style="display:flex; gap:8px; margin-top:12px;">
341
- <input type="hidden" name="action" value="add_loc">
342
- <input type="hidden" name="l_type" value="shops">
343
- <input type="text" name="l_name" class="form-control" style="margin:0;" placeholder="Название" required>
344
- <button type="submit" class="btn btn-primary" style="flex:0 0 auto; padding:0 20px;">+</button>
345
- </form>
346
- </div>
347
- <div class="settings-card">
348
- <h3>Сотрудники</h3>
349
- <div id="set_emps"></div>
350
- <form method="POST" style="display:flex; gap:8px; margin-top:12px;">
351
- <input type="hidden" name="action" value="add_emp">
352
- <input type="text" name="e_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>
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
- cat.innerHTML = '';
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" style="display:flex;align-items:center;justify-content:center;color:#9ca3af;">Нет фото</div>`;
510
 
511
  let html = `<div class="product">
512
  ${imgHtml}
@@ -539,8 +565,9 @@ MAIN_HTML = '''
539
  }
540
 
541
  html += `</div></div>`;
542
- cat.insertAdjacentHTML('beforeend', html);
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:4px 8px; cursor:pointer;" onclick="removeFromCart(${i})">×</button>
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" style="border:none;background:var(--danger);color:white;border-radius:6px;padding:4px 8px;cursor:pointer;">×</button></form></div>`).join('');
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" style="border:none;background:var(--danger);color:white;border-radius:6px;padding:4px 8px;cursor:pointer;">×</button></form></div>`).join('');
695
 
696
  const eCont = document.getElementById('set_emps');
697
- eCont.innerHTML = data.settings.employees.map(e => `<div class="settings-list-item"><div style="display:flex;flex-direction:column;gap:4px;"><span>${e.name}</span><button type="button" onclick="navigator.clipboard.writeText('${hostUrl}emp/${e.token}');alert('Ссылка скопирована')" style="font-size:0.8rem;padding:6px;border-radius:6px;border:none;background:#e5e7eb;cursor:pointer;font-weight:600;">Копировать ссылку</button></div><form method="POST"><input type="hidden" name="action" value="del_emp"><input type="hidden" name="e_id" value="${e.id}"><button type="submit" style="border:none;background:var(--danger);color:white;border-radius:6px;padding:4px 8px;cursor:pointer;">×</button></form></div>`).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698
  }
699
 
700
  function buildLocSelects() {
@@ -723,7 +765,7 @@ MAIN_HTML = '''
723
 
724
  function renderHistory() {
725
  const hc = document.getElementById('history_container');
726
- hc.innerHTML = '';
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
- hc.insertAdjacentHTML('beforeend', `<div class="history-item">
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)