Kgshop commited on
Commit
16dc90b
·
verified ·
1 Parent(s): 464718f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +421 -13
app.py CHANGED
@@ -33,6 +33,8 @@ KASSAS_FILE = os.path.join(DATA_DIR, 'kassas.json')
33
  EXPENSES_FILE = os.path.join(DATA_DIR, 'expenses.json')
34
  PERSONAL_EXPENSES_FILE = os.path.join(DATA_DIR, 'personal_expenses.json')
35
  SHIFTS_FILE = os.path.join(DATA_DIR, 'shifts.json')
 
 
36
 
37
  DATA_FILES = {
38
  'inventory': (INVENTORY_FILE, threading.Lock()),
@@ -42,6 +44,8 @@ DATA_FILES = {
42
  'expenses': (EXPENSES_FILE, threading.Lock()),
43
  'personal_expenses': (PERSONAL_EXPENSES_FILE, threading.Lock()),
44
  'shifts': (SHIFTS_FILE, threading.Lock()),
 
 
45
  }
46
 
47
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE", "YOUR_WRITE_TOKEN_HERE")
@@ -1612,6 +1616,126 @@ def admin_logout():
1612
  flash("Вы вышли из системы.", "info")
1613
  return redirect(url_for('sales_screen'))
1614
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1615
  BASE_TEMPLATE = """
1616
  <!DOCTYPE html>
1617
  <html lang="ru" data-bs-theme="light">
@@ -1665,6 +1789,7 @@ BASE_TEMPLATE = """
1665
  <li><a class="dropdown-item" href="{{ url_for('product_roi_report') }}">Окупаемость товаров</a></li>
1666
  </ul>
1667
  </li>
 
1668
  <li class="nav-item"><a class="nav-link {% if request.endpoint in ['cashier_login', 'cashier_dashboard'] %}active{% endif %}" href="{{ url_for('cashier_login') }}"><i class="fas fa-fw fa-user-circle me-2"></i>Кабинет кассира</a></li>
1669
  <li class="nav-item"><a class="nav-link {% if request.endpoint in ['admin_panel', 'admin_shifts'] %}active{% endif %}" href="{{ url_for('admin_panel') }}"><i class="fas fa-fw fa-cogs me-2"></i>Админка</a></li>
1670
  {% if session.admin_logged_in %}
@@ -1714,6 +1839,19 @@ BASE_TEMPLATE = """
1714
  </div>
1715
  <div class="form-text">Введите номер без +7.</div>
1716
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
1717
  <input type="hidden" id="receipt-url">
1718
  </div>
1719
  <div class="modal-footer">
@@ -1741,7 +1879,9 @@ BASE_TEMPLATE = """
1741
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
1742
  <script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
1743
  <script>
 
1744
  document.addEventListener('DOMContentLoaded', () => {
 
1745
  document.getElementById('sidebar-toggle')?.addEventListener('click', () => document.querySelector('.sidebar').classList.toggle('active'));
1746
  const themeToggle = document.getElementById('theme-toggle');
1747
  const getStoredTheme = () => localStorage.getItem('theme');
@@ -1757,6 +1897,92 @@ BASE_TEMPLATE = """
1757
  setStoredTheme(newTheme);
1758
  setTheme(newTheme);
1759
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1760
  });
1761
  </script>
1762
  __SCRIPTS__
@@ -1835,6 +2061,10 @@ SALES_SCREEN_CONTENT = """
1835
  </div>
1836
  <div class="d-grid gap-2">
1837
  <div class="btn-group"><button class="btn btn-success flex-grow-1" id="pay-cash-btn"><i class="fas fa-money-bill-wave me-2"></i>Наличные</button><button class="btn btn-info flex-grow-1" id="pay-card-btn"><i class="far fa-credit-card me-2"></i>Карта</button></div>
 
 
 
 
1838
  <button class="btn btn-danger" id="clear-cart-btn">Очистить</button>
1839
  </div>
1840
  </div>
@@ -1891,6 +2121,33 @@ SALES_SCREEN_CONTENT = """
1891
  </div>
1892
  </div>
1893
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1894
  """
1895
 
1896
  SALES_SCREEN_SCRIPTS = """
@@ -1902,11 +2159,12 @@ document.addEventListener('DOMContentLoaded', () => {
1902
  let audioCtx;
1903
  let isScannerPaused = false;
1904
 
1905
- const receiptModal = new bootstrap.Modal(document.getElementById('receiptModal'));
1906
  const variantSelectModal = new bootstrap.Modal(document.getElementById('variantSelectModal'));
1907
  const cashierLoginModal = new bootstrap.Modal(document.getElementById('cashierLoginModal'));
1908
  const startShiftModal = new bootstrap.Modal(document.getElementById('startShiftModal'));
1909
  const customItemModal = new bootstrap.Modal(document.getElementById('customItemModal'));
 
 
1910
  const allProducts = {{ inventory|tojson|safe }};
1911
 
1912
  const session = {
@@ -2199,18 +2457,6 @@ document.addEventListener('DOMContentLoaded', () => {
2199
 
2200
  document.getElementById('pay-cash-btn').addEventListener('click', () => completeSale('cash'));
2201
  document.getElementById('pay-card-btn').addEventListener('click', () => completeSale('card'));
2202
-
2203
- document.getElementById('send-whatsapp-btn').addEventListener('click', () => {
2204
- const phone = document.getElementById('whatsapp-phone').value.replace(/\\D/g, '');
2205
- const receiptUrl = document.getElementById('receipt-url').value;
2206
- if (phone && receiptUrl) {
2207
- const fullPhone = '7' + phone;
2208
- const message = encodeURIComponent(`Ваша накладная: ${receiptUrl}`);
2209
- window.open(`https://wa.me/${fullPhone}?text=${message}`, '_blank');
2210
- } else {
2211
- alert('Введите номер телефона.');
2212
- }
2213
- });
2214
 
2215
  const html5QrCode = new Html5Qrcode("reader");
2216
  const scannerStatusEl = document.getElementById('scanner-status');
@@ -2431,8 +2677,121 @@ document.addEventListener('DOMContentLoaded', () => {
2431
  }
2432
  });
2433
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2434
  updateCartView();
2435
  initializeSession();
 
2436
  });
2437
  </script>
2438
  """
@@ -3466,6 +3825,55 @@ RETURN_PAGE_CONTENT = """
3466
  </div>
3467
  """
3468
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3469
  if __name__ == '__main__':
3470
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
3471
  backup_thread.start()
 
33
  EXPENSES_FILE = os.path.join(DATA_DIR, 'expenses.json')
34
  PERSONAL_EXPENSES_FILE = os.path.join(DATA_DIR, 'personal_expenses.json')
35
  SHIFTS_FILE = os.path.join(DATA_DIR, 'shifts.json')
36
+ HELD_BILLS_FILE = os.path.join(DATA_DIR, 'held_bills.json')
37
+ CUSTOMERS_FILE = os.path.join(DATA_DIR, 'customers.json')
38
 
39
  DATA_FILES = {
40
  'inventory': (INVENTORY_FILE, threading.Lock()),
 
44
  'expenses': (EXPENSES_FILE, threading.Lock()),
45
  'personal_expenses': (PERSONAL_EXPENSES_FILE, threading.Lock()),
46
  'shifts': (SHIFTS_FILE, threading.Lock()),
47
+ 'held_bills': (HELD_BILLS_FILE, threading.Lock()),
48
+ 'customers': (CUSTOMERS_FILE, threading.Lock()),
49
  }
50
 
51
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE", "YOUR_WRITE_TOKEN_HERE")
 
1616
  flash("Вы вышли из системы.", "info")
1617
  return redirect(url_for('sales_screen'))
1618
 
1619
+ @app.route('/api/held_bills', methods=['GET'])
1620
+ def get_held_bills():
1621
+ bills = load_json_data('held_bills')
1622
+ bills.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
1623
+ return jsonify(bills)
1624
+
1625
+ @app.route('/api/hold_bill', methods=['POST'])
1626
+ def hold_bill():
1627
+ data = request.json
1628
+ if not data or 'name' not in data or 'cart' not in data:
1629
+ return jsonify({'success': False, 'message': 'Invalid data'}), 400
1630
+
1631
+ bills = load_json_data('held_bills')
1632
+ new_bill = {
1633
+ 'id': uuid.uuid4().hex,
1634
+ 'name': data['name'],
1635
+ 'cart': data['cart'],
1636
+ 'timestamp': get_current_time().isoformat()
1637
+ }
1638
+ bills.append(new_bill)
1639
+ save_json_data('held_bills', bills)
1640
+ upload_db_to_hf('held_bills')
1641
+ return jsonify({'success': True, 'bill': new_bill})
1642
+
1643
+ @app.route('/api/restore_bill/<bill_id>', methods=['POST'])
1644
+ def restore_bill(bill_id):
1645
+ bills = load_json_data('held_bills')
1646
+ bill_to_restore = find_item_by_field(bills, 'id', bill_id)
1647
+ if not bill_to_restore:
1648
+ return jsonify({'success': False, 'message': 'Bill not found'}), 404
1649
+
1650
+ remaining_bills = [b for b in bills if b.get('id') != bill_id]
1651
+ save_json_data('held_bills', remaining_bills)
1652
+ upload_db_to_hf('held_bills')
1653
+ return jsonify({'success': True, 'bill': bill_to_restore})
1654
+
1655
+ @app.route('/api/hold_bill/<bill_id>', methods=['DELETE'])
1656
+ def delete_held_bill(bill_id):
1657
+ bills = load_json_data('held_bills')
1658
+ initial_len = len(bills)
1659
+ bills = [b for b in bills if b.get('id') != bill_id]
1660
+ if len(bills) < initial_len:
1661
+ save_json_data('held_bills', bills)
1662
+ upload_db_to_hf('held_bills')
1663
+ return jsonify({'success': True})
1664
+ else:
1665
+ return jsonify({'success': False, 'message': 'Bill not found'}), 404
1666
+
1667
+ @app.route('/customers')
1668
+ @admin_required
1669
+ def customer_management():
1670
+ customers = load_json_data('customers')
1671
+ customers.sort(key=lambda x: x.get('name', '').lower())
1672
+ html = BASE_TEMPLATE.replace('__TITLE__', "Клиенты").replace('__CONTENT__', CUSTOMERS_CONTENT).replace('__SCRIPTS__', '')
1673
+ return render_template_string(html, customers=customers)
1674
+
1675
+ @app.route('/admin/customer', methods=['POST'])
1676
+ @admin_required
1677
+ def manage_customer():
1678
+ action = request.form.get('action')
1679
+ customers = load_json_data('customers')
1680
+
1681
+ if action == 'add':
1682
+ name = request.form.get('name', '').strip()
1683
+ phone = request.form.get('phone', '').strip().replace('+', '').replace(' ', '')
1684
+ if name and phone:
1685
+ if not find_item_by_field(customers, 'phone', phone):
1686
+ new_customer = {'id': uuid.uuid4().hex, 'name': name, 'phone': phone}
1687
+ customers.append(new_customer)
1688
+ flash(f"Клиент '{name}' добавлен.", "success")
1689
+ else:
1690
+ flash(f"Клиент с номером {phone} уже существует.", "warning")
1691
+ else:
1692
+ flash("Имя и телефон обязательны.", "danger")
1693
+
1694
+ elif action == 'delete':
1695
+ customer_id = request.form.get('id')
1696
+ initial_len = len(customers)
1697
+ customers = [c for c in customers if c.get('id') != customer_id]
1698
+ if len(customers) < initial_len:
1699
+ flash("Клиент удален.", "success")
1700
+ else:
1701
+ flash("Клиент не найден.", "warning")
1702
+
1703
+ save_json_data('customers', customers)
1704
+ upload_db_to_hf('customers')
1705
+ return redirect(url_for('customer_management'))
1706
+
1707
+ @app.route('/api/customers/search')
1708
+ def search_customers():
1709
+ query = request.args.get('q', '').lower()
1710
+ if not query:
1711
+ return jsonify([])
1712
+ customers = load_json_data('customers')
1713
+ matches = [
1714
+ c for c in customers
1715
+ if query in c.get('name', '').lower() or query in c.get('phone', '')
1716
+ ]
1717
+ return jsonify(matches[:10])
1718
+
1719
+ @app.route('/api/customers/save', methods=['POST'])
1720
+ def save_customer():
1721
+ data = request.json
1722
+ name = data.get('name', '').strip()
1723
+ phone = data.get('phone', '').strip()
1724
+ if not name or not phone:
1725
+ return jsonify({'success': False, 'message': 'Name and phone required'}), 400
1726
+
1727
+ customers = load_json_data('customers')
1728
+ existing_customer = find_item_by_field(customers, 'phone', phone)
1729
+ if existing_customer:
1730
+ existing_customer['name'] = name
1731
+ else:
1732
+ new_customer = {'id': uuid.uuid4().hex, 'name': name, 'phone': phone}
1733
+ customers.append(new_customer)
1734
+
1735
+ save_json_data('customers', customers)
1736
+ upload_db_to_hf('customers')
1737
+ return jsonify({'success': True})
1738
+
1739
  BASE_TEMPLATE = """
1740
  <!DOCTYPE html>
1741
  <html lang="ru" data-bs-theme="light">
 
1789
  <li><a class="dropdown-item" href="{{ url_for('product_roi_report') }}">Окупаемость товаров</a></li>
1790
  </ul>
1791
  </li>
1792
+ <li class="nav-item"><a class="nav-link {% if request.endpoint == 'customer_management' %}active{% endif %}" href="{{ url_for('customer_management') }}"><i class="fas fa-fw fa-users me-2"></i>Клиенты</a></li>
1793
  <li class="nav-item"><a class="nav-link {% if request.endpoint in ['cashier_login', 'cashier_dashboard'] %}active{% endif %}" href="{{ url_for('cashier_login') }}"><i class="fas fa-fw fa-user-circle me-2"></i>Кабинет кассира</a></li>
1794
  <li class="nav-item"><a class="nav-link {% if request.endpoint in ['admin_panel', 'admin_shifts'] %}active{% endif %}" href="{{ url_for('admin_panel') }}"><i class="fas fa-fw fa-cogs me-2"></i>Админка</a></li>
1795
  {% if session.admin_logged_in %}
 
1839
  </div>
1840
  <div class="form-text">Введите номер без +7.</div>
1841
  </div>
1842
+ <div class="mb-3">
1843
+ <label for="customer-search" class="form-label">Поиск клиента</label>
1844
+ <input type="text" id="customer-search" class="form-control" placeholder="Начните вводить имя...">
1845
+ <div id="customer-search-results" class="list-group mt-1" style="max-height: 150px; overflow-y: auto;"></div>
1846
+ </div>
1847
+ <div class="form-check">
1848
+ <input class="form-check-input" type="checkbox" id="save-customer-checkbox">
1849
+ <label class="form-check-label" for="save-customer-checkbox">Сохранить/обновить клиента</label>
1850
+ </div>
1851
+ <div class="mb-3 mt-2" id="customer-name-container" style="display:none;">
1852
+ <label for="customer-name-input" class="form-label">Имя клиента</label>
1853
+ <input type="text" class="form-control" id="customer-name-input">
1854
+ </div>
1855
  <input type="hidden" id="receipt-url">
1856
  </div>
1857
  <div class="modal-footer">
 
1879
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
1880
  <script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
1881
  <script>
1882
+ let receiptModal;
1883
  document.addEventListener('DOMContentLoaded', () => {
1884
+ receiptModal = new bootstrap.Modal(document.getElementById('receiptModal'));
1885
  document.getElementById('sidebar-toggle')?.addEventListener('click', () => document.querySelector('.sidebar').classList.toggle('active'));
1886
  const themeToggle = document.getElementById('theme-toggle');
1887
  const getStoredTheme = () => localStorage.getItem('theme');
 
1897
  setStoredTheme(newTheme);
1898
  setTheme(newTheme);
1899
  });
1900
+
1901
+ const receiptModalEl = document.getElementById('receiptModal');
1902
+ if (receiptModalEl) {
1903
+ const saveCustomerCheckbox = document.getElementById('save-customer-checkbox');
1904
+ const customerNameContainer = document.getElementById('customer-name-container');
1905
+ const customerSearchInput = document.getElementById('customer-search');
1906
+ const customerSearchResults = document.getElementById('customer-search-results');
1907
+ const whatsappPhoneInput = document.getElementById('whatsapp-phone');
1908
+ const customerNameInput = document.getElementById('customer-name-input');
1909
+ const sendWhatsappBtn = document.getElementById('send-whatsapp-btn');
1910
+
1911
+ saveCustomerCheckbox.addEventListener('change', () => {
1912
+ customerNameContainer.style.display = saveCustomerCheckbox.checked ? 'block' : 'none';
1913
+ });
1914
+
1915
+ let searchTimeout;
1916
+ customerSearchInput.addEventListener('input', () => {
1917
+ clearTimeout(searchTimeout);
1918
+ const query = customerSearchInput.value.trim();
1919
+ if (query.length < 2) {
1920
+ customerSearchResults.innerHTML = '';
1921
+ return;
1922
+ }
1923
+ searchTimeout = setTimeout(() => {
1924
+ fetch(`/api/customers/search?q=${encodeURIComponent(query)}`)
1925
+ .then(res => res.json())
1926
+ .then(customers => {
1927
+ customerSearchResults.innerHTML = '';
1928
+ if (customers.length > 0) {
1929
+ customers.forEach(customer => {
1930
+ const item = document.createElement('a');
1931
+ item.href = '#';
1932
+ item.className = 'list-group-item list-group-item-action';
1933
+ item.textContent = `${customer.name} - ${customer.phone}`;
1934
+ item.addEventListener('click', (e) => {
1935
+ e.preventDefault();
1936
+ whatsappPhoneInput.value = customer.phone;
1937
+ customerNameInput.value = customer.name;
1938
+ customerSearchInput.value = customer.name;
1939
+ customerSearchResults.innerHTML = '';
1940
+ });
1941
+ customerSearchResults.appendChild(item);
1942
+ });
1943
+ } else {
1944
+ customerSearchResults.innerHTML = '<div class="list-group-item">Клиенты не найдены.</div>';
1945
+ }
1946
+ });
1947
+ }, 300);
1948
+ });
1949
+
1950
+ sendWhatsappBtn.addEventListener('click', () => {
1951
+ const phone = whatsappPhoneInput.value.replace(/\\D/g, '');
1952
+ const receiptUrl = document.getElementById('receipt-url').value;
1953
+ if (!phone || !receiptUrl) {
1954
+ alert('Введите номер телефона.');
1955
+ return;
1956
+ }
1957
+ const fullPhone = '7' + phone;
1958
+ const message = encodeURIComponent(`Ваша накладная: ${receiptUrl}`);
1959
+ window.open(`https://wa.me/${fullPhone}?text=${message}`, '_blank');
1960
+
1961
+ if (saveCustomerCheckbox.checked) {
1962
+ const name = customerNameInput.value.trim();
1963
+ if (name && phone) {
1964
+ fetch('/api/customers/save', {
1965
+ method: 'POST',
1966
+ headers: {'Content-Type': 'application/json'},
1967
+ body: JSON.stringify({name: name, phone: phone})
1968
+ })
1969
+ .then(res => res.json())
1970
+ .then(data => {
1971
+ if (!data.success) console.error('Failed to save customer:', data.message);
1972
+ });
1973
+ }
1974
+ }
1975
+ });
1976
+
1977
+ receiptModalEl.addEventListener('show.bs.modal', () => {
1978
+ whatsappPhoneInput.value = '';
1979
+ customerSearchInput.value = '';
1980
+ customerNameInput.value = '';
1981
+ customerSearchResults.innerHTML = '';
1982
+ saveCustomerCheckbox.checked = false;
1983
+ customerNameContainer.style.display = 'none';
1984
+ });
1985
+ }
1986
  });
1987
  </script>
1988
  __SCRIPTS__
 
2061
  </div>
2062
  <div class="d-grid gap-2">
2063
  <div class="btn-group"><button class="btn btn-success flex-grow-1" id="pay-cash-btn"><i class="fas fa-money-bill-wave me-2"></i>Наличные</button><button class="btn btn-info flex-grow-1" id="pay-card-btn"><i class="far fa-credit-card me-2"></i>Карта</button></div>
2064
+ <div class="btn-group">
2065
+ <button class="btn btn-secondary" id="hold-bill-btn"><i class="fas fa-pause me-2"></i>Отложить накладную</button>
2066
+ <button class="btn btn-secondary" id="list-held-bills-btn"><i class="fas fa-list me-2"></i>Отложенные <span id="held-bills-count" class="badge bg-primary ms-1" style="display:none;"></span></button>
2067
+ </div>
2068
  <button class="btn btn-danger" id="clear-cart-btn">Очистить</button>
2069
  </div>
2070
  </div>
 
2121
  </div>
2122
  </div>
2123
  </div>
2124
+
2125
+ <div class="modal fade" id="holdBillModal" tabindex="-1">
2126
+ <div class="modal-dialog">
2127
+ <div class="modal-content">
2128
+ <div class="modal-header"><h5 class="modal-title">Отложить накладную</h5></div>
2129
+ <form id="hold-bill-form">
2130
+ <div class="modal-body">
2131
+ <label for="hold-bill-name" class="form-label">Название для этой накладной</label>
2132
+ <input type="text" id="hold-bill-name" class="form-control" placeholder="Напр. 'Стол 5', 'Мария'" required>
2133
+ </div>
2134
+ <div class="modal-footer"><button type="submit" class="btn btn-primary">Отложить</button></div>
2135
+ </form>
2136
+ </div>
2137
+ </div>
2138
+ </div>
2139
+
2140
+ <div class="modal fade" id="heldBillsListModal" tabindex="-1">
2141
+ <div class="modal-dialog modal-lg">
2142
+ <div class="modal-content">
2143
+ <div class="modal-header"><h5 class="modal-title">Отложенные накладные</h5></div>
2144
+ <div class="modal-body">
2145
+ <div id="held-bills-list" class="list-group">
2146
+ </div>
2147
+ </div>
2148
+ </div>
2149
+ </div>
2150
+ </div>
2151
  """
2152
 
2153
  SALES_SCREEN_SCRIPTS = """
 
2159
  let audioCtx;
2160
  let isScannerPaused = false;
2161
 
 
2162
  const variantSelectModal = new bootstrap.Modal(document.getElementById('variantSelectModal'));
2163
  const cashierLoginModal = new bootstrap.Modal(document.getElementById('cashierLoginModal'));
2164
  const startShiftModal = new bootstrap.Modal(document.getElementById('startShiftModal'));
2165
  const customItemModal = new bootstrap.Modal(document.getElementById('customItemModal'));
2166
+ const holdBillModal = new bootstrap.Modal(document.getElementById('holdBillModal'));
2167
+ const heldBillsListModal = new bootstrap.Modal(document.getElementById('heldBillsListModal'));
2168
  const allProducts = {{ inventory|tojson|safe }};
2169
 
2170
  const session = {
 
2457
 
2458
  document.getElementById('pay-cash-btn').addEventListener('click', () => completeSale('cash'));
2459
  document.getElementById('pay-card-btn').addEventListener('click', () => completeSale('card'));
 
 
 
 
 
 
 
 
 
 
 
 
2460
 
2461
  const html5QrCode = new Html5Qrcode("reader");
2462
  const scannerStatusEl = document.getElementById('scanner-status');
 
2677
  }
2678
  });
2679
 
2680
+ const updateHeldBillsCount = () => {
2681
+ fetch('/api/held_bills')
2682
+ .then(res => res.json())
2683
+ .then(data => {
2684
+ const count = data.length;
2685
+ const badge = document.getElementById('held-bills-count');
2686
+ if (count > 0) {
2687
+ badge.textContent = count;
2688
+ badge.style.display = '';
2689
+ } else {
2690
+ badge.style.display = 'none';
2691
+ }
2692
+ });
2693
+ };
2694
+
2695
+ document.getElementById('hold-bill-btn').addEventListener('click', () => {
2696
+ if (Object.keys(cart).length === 0) {
2697
+ alert('Корзина пуста. Нечего откладывать.');
2698
+ return;
2699
+ }
2700
+ document.getElementById('hold-bill-form').reset();
2701
+ holdBillModal.show();
2702
+ });
2703
+
2704
+ document.getElementById('hold-bill-form').addEventListener('submit', (e) => {
2705
+ e.preventDefault();
2706
+ const name = document.getElementById('hold-bill-name').value.trim();
2707
+ if (!name) return;
2708
+
2709
+ fetch('/api/hold_bill', {
2710
+ method: 'POST',
2711
+ headers: {'Content-Type': 'application/json'},
2712
+ body: JSON.stringify({ name: name, cart: cart })
2713
+ })
2714
+ .then(res => res.json())
2715
+ .then(data => {
2716
+ if (data.success) {
2717
+ for(const id in cart) delete cart[id];
2718
+ updateCartView();
2719
+ updateHeldBillsCount();
2720
+ holdBillModal.hide();
2721
+ } else {
2722
+ alert('Ошибка: ' + data.message);
2723
+ }
2724
+ });
2725
+ });
2726
+
2727
+ document.getElementById('list-held-bills-btn').addEventListener('click', () => {
2728
+ fetch('/api/held_bills')
2729
+ .then(res => res.json())
2730
+ .then(bills => {
2731
+ const listEl = document.getElementById('held-bills-list');
2732
+ listEl.innerHTML = '';
2733
+ if (bills.length === 0) {
2734
+ listEl.innerHTML = '<p class="text-center text-muted">Нет отложенных накладных.</p>';
2735
+ } else {
2736
+ bills.forEach(bill => {
2737
+ listEl.innerHTML += `
2738
+ <div class="list-group-item d-flex justify-content-between align-items-center">
2739
+ <span><strong>${bill.name}</strong> - ${new Date(bill.timestamp).toLocaleTimeString('ru-RU')}</span>
2740
+ <div>
2741
+ <button class="btn btn-sm btn-success restore-bill-btn" data-id="${bill.id}">Восстановить</button>
2742
+ <button class="btn btn-sm btn-danger delete-held-bill-btn ms-2" data-id="${bill.id}">Удалить</button>
2743
+ </div>
2744
+ </div>`;
2745
+ });
2746
+ }
2747
+ heldBillsListModal.show();
2748
+ });
2749
+ });
2750
+
2751
+ document.getElementById('held-bills-list').addEventListener('click', (e) => {
2752
+ const billId = e.target.dataset.id;
2753
+ if (!billId) return;
2754
+
2755
+ if (e.target.classList.contains('restore-bill-btn')) {
2756
+ if (Object.keys(cart).length > 0 && !confirm('Текущая корзина будет заменена. Продолжить?')) {
2757
+ return;
2758
+ }
2759
+ fetch(`/api/restore_bill/${billId}`, { method: 'POST' })
2760
+ .then(res => res.json())
2761
+ .then(data => {
2762
+ if (data.success) {
2763
+ for(const id in cart) delete cart[id];
2764
+ Object.assign(cart, data.bill.cart);
2765
+ updateCartView();
2766
+ updateHeldBillsCount();
2767
+ heldBillsListModal.hide();
2768
+ } else {
2769
+ alert('Ошибка: ' + data.message);
2770
+ }
2771
+ });
2772
+ }
2773
+
2774
+ if (e.target.classList.contains('delete-held-bill-btn')) {
2775
+ if (!confirm('Удалить эту отложенную накладную?')) return;
2776
+ fetch(`/api/hold_bill/${billId}`, { method: 'DELETE' })
2777
+ .then(res => res.json())
2778
+ .then(data => {
2779
+ if (data.success) {
2780
+ e.target.closest('.list-group-item').remove();
2781
+ updateHeldBillsCount();
2782
+ if (document.getElementById('held-bills-list').children.length === 0) {
2783
+ document.getElementById('held-bills-list').innerHTML = '<p class="text-center text-muted">Нет отложенных накладных.</p>';
2784
+ }
2785
+ } else {
2786
+ alert('Ошибка: ' + data.message);
2787
+ }
2788
+ });
2789
+ }
2790
+ });
2791
+
2792
  updateCartView();
2793
  initializeSession();
2794
+ updateHeldBillsCount();
2795
  });
2796
  </script>
2797
  """
 
3825
  </div>
3826
  """
3827
 
3828
+ CUSTOMERS_CONTENT = """
3829
+ <div class="card">
3830
+ <div class="card-header d-flex justify-content-between align-items-center">
3831
+ <h5 class="mb-0">Клиенты</h5>
3832
+ <button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addCustomerModal"><i class="fas fa-plus me-2"></i>Добавить клиента</button>
3833
+ </div>
3834
+ <div class="card-body">
3835
+ <div class="table-responsive">
3836
+ <table class="table table-hover">
3837
+ <thead><tr><th>Имя</th><th>Телефон</th><th></th></tr></thead>
3838
+ <tbody>
3839
+ {% for c in customers %}
3840
+ <tr>
3841
+ <td>{{ c.name }}</td>
3842
+ <td>{{ c.phone }}</td>
3843
+ <td class="text-end">
3844
+ <form action="{{ url_for('manage_customer') }}" method="POST" onsubmit="return confirm('Удалить этого клиента?');">
3845
+ <input type="hidden" name="action" value="delete">
3846
+ <input type="hidden" name="id" value="{{ c.id }}">
3847
+ <button type="submit" class="btn btn-sm btn-outline-danger"><i class="fas fa-trash"></i></button>
3848
+ </form>
3849
+ </td>
3850
+ </tr>
3851
+ {% else %}
3852
+ <tr><td colspan="3" class="text-center">Список клиентов пуст.</td></tr>
3853
+ {% endfor %}
3854
+ </tbody>
3855
+ </table>
3856
+ </div>
3857
+ </div>
3858
+ </div>
3859
+
3860
+ <div class="modal fade" id="addCustomerModal" tabindex="-1">
3861
+ <div class="modal-dialog">
3862
+ <div class="modal-content">
3863
+ <div class="modal-header"><h5 class="modal-title">Новый клиент</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
3864
+ <form action="{{ url_for('manage_customer') }}" method="POST">
3865
+ <input type="hidden" name="action" value="add">
3866
+ <div class="modal-body">
3867
+ <div class="mb-3"><label class="form-label">Имя</label><input type="text" name="name" class="form-control" required></div>
3868
+ <div class="mb-3"><label class="form-label">Телефон</label><input type="tel" name="phone" class="form-control" placeholder="7071234567" required></div>
3869
+ </div>
3870
+ <div class="modal-footer"><button type="submit" class="btn btn-primary">Сохранить</button></div>
3871
+ </form>
3872
+ </div>
3873
+ </div>
3874
+ </div>
3875
+ """
3876
+
3877
  if __name__ == '__main__':
3878
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
3879
  backup_thread.start()