diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 439bd2a..ee499ae 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index ceb4e69..1ab259a 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/templates/core/pos.html b/core/templates/core/pos.html index 2be11e6..055849e 100644 --- a/core/templates/core/pos.html +++ b/core/templates/core/pos.html @@ -191,12 +191,7 @@
- +
@@ -1013,6 +1008,8 @@ if (data.success) { cart = data.items; document.getElementById('customerSelect').value = data.customer_id || ""; + document.getElementById('customerSearchInput').value = data.customer_name || ""; + document.getElementById('clearCustomerBtn').style.display = data.customer_id ? 'block' : 'none'; renderCart(); onCustomerChange(); updateHeldCount(); @@ -1082,6 +1079,74 @@ } }); } +// Customer Search Logic +let searchTimeout; + +document.getElementById('customerSearchInput').addEventListener('input', function(e) { + const query = e.target.value.trim(); + const resultsContainer = document.getElementById('customerSearchResults'); + const clearBtn = document.getElementById('clearCustomerBtn'); + + if (query.length > 0) { + clearBtn.style.display = 'block'; + } else { + clearBtn.style.display = 'none'; + resultsContainer.classList.add('d-none'); + return; + } + + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + fetch(`{% url 'search_customers_api' %}?q=${encodeURIComponent(query)}`) + .then(res => res.json()) + .then(data => { + resultsContainer.innerHTML = ''; + if (data.results.length > 0) { + data.results.forEach(c => { + const item = document.createElement('button'); + item.className = 'list-group-item list-group-item-action py-2 text-start'; + item.innerHTML = ` +
+
${c.name}
+
${c.phone || ''}
+
+ `; + item.onclick = () => selectCustomer(c.id, c.name); + resultsContainer.appendChild(item); + }); + resultsContainer.classList.remove('d-none'); + } else { + resultsContainer.innerHTML = '
{% trans "No results found" %}
'; + resultsContainer.classList.remove('d-none'); + } + }); + }, 300); +}); + +function selectCustomer(id, name) { + document.getElementById('customerSelect').value = id; + document.getElementById('customerSearchInput').value = name; + document.getElementById('customerSearchResults').classList.add('d-none'); + document.getElementById('clearCustomerBtn').style.display = 'block'; + onCustomerChange(); +} + +function clearCustomerSelection() { + document.getElementById('customerSelect').value = ''; + document.getElementById('customerSearchInput').value = ''; + document.getElementById('customerSearchResults').classList.add('d-none'); + document.getElementById('clearCustomerBtn').style.display = 'none'; + onCustomerChange(); +} + +document.addEventListener('click', function(e) { + const container = document.getElementById('customerSearchResults'); + const input = document.getElementById('customerSearchInput'); + const clearBtn = document.getElementById('clearCustomerBtn'); + if (container && !container.contains(e.target) && e.target !== input && e.target !== clearBtn) { + container.classList.add('d-none'); + } +}); {% endlocalize %} {% endblock %} \ No newline at end of file diff --git a/core/templates/core/settings.html b/core/templates/core/settings.html index 12e5c38..f62b8d0 100644 --- a/core/templates/core/settings.html +++ b/core/templates/core/settings.html @@ -35,6 +35,11 @@ {% trans "Payment Methods" %} +
+ +
+
+
+
{% trans "Hardware Devices" %}
+ +
+
+
+ + + + + + + + + + + + {% for device in devices %} + + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Name" %}{% trans "Type" %}{% trans "Connection" %}{% trans "Status" %}{% trans "Actions" %}
{{ device.name }}{{ device.get_device_type_display }} + {{ device.get_connection_type_display }} + {% if device.ip_address %} + {{ device.ip_address }}{% if device.port %}:{{ device.port }}{% endif %} + {% endif %} + + {% if device.is_active %} + {% trans "Active" %} + {% else %} + {% trans "Inactive" %} + {% endif %} + + + +
+
+ + {% trans "No devices configured." %} +
+
+
+
+
+
+
@@ -557,6 +703,63 @@
+ + + {% endblock %} {% block scripts %} diff --git a/core/urls.py b/core/urls.py index c274525..06d1e02 100644 --- a/core/urls.py +++ b/core/urls.py @@ -80,6 +80,7 @@ urlpatterns = [ path('customers/edit//', views.edit_customer, name='edit_customer'), path('customers/delete//', views.delete_customer, name='delete_customer'), path('api/add-customer-ajax/', views.add_customer_ajax, name='add_customer_ajax'), + path('api/search-customers/', views.search_customers_api, name='search_customers_api'), # Suppliers path('suppliers/add/', views.add_supplier, name='add_supplier'), @@ -118,7 +119,13 @@ urlpatterns = [ path('settings/loyalty/edit//', views.edit_loyalty_tier, name='edit_loyalty_tier'), path('settings/loyalty/delete//', views.delete_loyalty_tier, name='delete_loyalty_tier'), path('api/customer-loyalty//', views.get_customer_loyalty_api, name='get_customer_loyalty_api'), + # WhatsApp path('api/send-invoice-whatsapp/', views.send_invoice_whatsapp, name='send_invoice_whatsapp'), path('api/test-whatsapp/', views.test_whatsapp_connection, name='test_whatsapp_connection'), + + # Devices + path('settings/devices/add/', views.add_device, name='add_device'), + path('settings/devices/edit//', views.edit_device, name='edit_device'), + path('settings/devices/delete//', views.delete_device, name='delete_device'), ] diff --git a/core/views.py b/core/views.py index fb5606a..8c556ec 100644 --- a/core/views.py +++ b/core/views.py @@ -23,7 +23,7 @@ from .models import ( Expense, ExpenseCategory, Quotation, QuotationItem, SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem, PaymentMethod, HeldSale, LoyaltyTier, LoyaltyTransaction -) +, Device) import json from datetime import timedelta from django.utils import timezone @@ -1019,11 +1019,13 @@ def settings_view(request): payment_methods = PaymentMethod.objects.all().order_by("name_en") loyalty_tiers = LoyaltyTier.objects.all().order_by("min_points") + devices = Device.objects.all().order_by("name") context = { "settings": settings, "payment_methods": payment_methods, - "loyalty_tiers": loyalty_tiers + "loyalty_tiers": loyalty_tiers, + "devices": devices } return render(request, "core/settings.html", context) @@ -1645,6 +1647,7 @@ def recall_held_sale_api(request, pk): data = { 'success': True, 'customer_id': held_sale.customer.id if held_sale.customer else None, + 'customer_name': held_sale.customer.name if held_sale.customer else "", 'items': held_sale.cart_data, 'total_amount': float(held_sale.total_amount), 'notes': held_sale.notes @@ -2355,3 +2358,62 @@ def test_whatsapp_connection(request): except Exception as e: return JsonResponse({'success': False, 'error': str(e)}) return JsonResponse({'success': False, 'error': _("Invalid request method.")}) + + +@login_required +def add_device(request): + if request.method == 'POST': + name = request.POST.get('name') + device_type = request.POST.get('device_type') + connection_type = request.POST.get('connection_type') + ip_address = request.POST.get('ip_address') + port = request.POST.get('port') + is_active = request.POST.get('is_active') == 'on' + + Device.objects.create( + name=name, + device_type=device_type, + connection_type=connection_type, + ip_address=ip_address if ip_address else None, + port=port if port else None, + is_active=is_active + ) + messages.success(request, _("Device added successfully!")) + return redirect(reverse('settings') + '#devices') + +@login_required +def edit_device(request, pk): + device = get_object_or_404(Device, pk=pk) + if request.method == 'POST': + device.name = request.POST.get('name') + device.device_type = request.POST.get('device_type') + device.connection_type = request.POST.get('connection_type') + device.ip_address = request.POST.get('ip_address') + device.port = request.POST.get('port') + device.is_active = request.POST.get('is_active') == 'on' + + if not device.ip_address: + device.ip_address = None + if not device.port: + device.port = None + + device.save() + messages.success(request, _("Device updated successfully!")) + return redirect(reverse('settings') + '#devices') + +@login_required +def delete_device(request, pk): + device = get_object_or_404(Device, pk=pk) + device.delete() + messages.success(request, _("Device deleted successfully!")) + return redirect(reverse('settings') + '#devices') +@login_required +def search_customers_api(request): + query = request.GET.get('q', '') + if query: + customers = Customer.objects.filter( + Q(name__icontains=query) | Q(phone__icontains=query) + ).values('id', 'name', 'phone')[:20] + else: + customers = [] + return JsonResponse({'results': list(customers)}) \ No newline at end of file diff --git a/core/views_patch.py b/core/views_patch.py deleted file mode 100644 index f64efac..0000000 --- a/core/views_patch.py +++ /dev/null @@ -1,35 +0,0 @@ -@login_required -def edit_product(request, pk): - product = get_object_or_404(Product, pk=pk) - if request.method == 'POST': - product.name_en = request.POST.get('name_en') - product.name_ar = request.POST.get('name_ar') - product.sku = request.POST.get('sku') - product.category = get_object_or_404(Category, id=request.POST.get('category')) - - unit_id = request.POST.get('unit') - product.unit = get_object_or_404(Unit, id=unit_id) if unit_id else None - - supplier_id = request.POST.get('supplier') - product.supplier = get_object_or_404(Supplier, id=supplier_id) if supplier_id else None - - product.cost_price = request.POST.get('cost_price', 0) - product.price = request.POST.get('price', 0) - product.vat = request.POST.get('vat', 0) - product.description = request.POST.get('description', '') - product.opening_stock = request.POST.get('opening_stock', 0) - product.stock_quantity = request.POST.get('stock_quantity', 0) - product.min_stock_level = request.POST.get('min_stock_level', 0) - product.is_active = request.POST.get('is_active') == 'on' - product.has_expiry = request.POST.get('has_expiry') == 'on' - product.expiry_date = request.POST.get('expiry_date') - if not product.has_expiry: - product.expiry_date = None - - if 'image' in request.FILES: - product.image = request.FILES['image'] - - product.save() - messages.success(request, _("Product updated successfully!")) - return redirect(reverse('inventory') + '#items') - return redirect(reverse('inventory') + '#items') diff --git a/debug_settings.py b/debug_settings.py new file mode 100644 index 0000000..a79282e --- /dev/null +++ b/debug_settings.py @@ -0,0 +1,38 @@ + +file_path = 'core/templates/core/settings.html' +with open(file_path, 'r') as f: + content = f.read() + +print("File length:", len(content)) + +# Check context for Nav Tab +if 'id="devices-tab"' in content: + print("Devices tab already exists.") +else: + context_str = 'id="whatsapp-tab" data-bs-toggle="pill" data-bs-target="#whatsapp" type="button" role="tab"> + {% trans "WhatsApp Gateway" %} + + ' + if context_str in content: + print("Found Nav Tab context.") + else: + print("Nav Tab context NOT found. Dumping nearby content:") + # Find rough location + idx = content.find('id="whatsapp-tab"') + if idx != -1: + print(content[idx:idx+300]) + +# Check context for Tab Pane +if 'id="devices" role="tabpanel"' in content: + print("Devices pane already exists.") +else: + # Try to find the end of tab content + # Look for Add Tier Modal + idx = content.find('') + if idx != -1: + print("Found Add Tier Modal at index:", idx) + print("Preceding content:") + print(content[idx-100:idx]) + else: + print("Add Tier Modal NOT found.") + diff --git a/patch_settings_html.py b/patch_settings_html.py new file mode 100644 index 0000000..8669a41 --- /dev/null +++ b/patch_settings_html.py @@ -0,0 +1,260 @@ +file_path = 'core/templates/core/settings.html' + +with open(file_path, 'r') as f: + content = f.read() + +# 1. Add Nav Tab +if 'id="devices-tab"' not in content: + whatsapp_tab_end = 'id="whatsapp-tab" data-bs-toggle="pill" data-bs-target="#whatsapp" type="button" role="tab">\n {% trans "WhatsApp Gateway" %}\n \n li>' + + insert_str = """ + " + + if whatsapp_tab_end in content: + content = content.replace(whatsapp_tab_end, whatsapp_tab_end + insert_str) + print("Added Devices Tab Nav.") + else: + # Fallback search if exact string match fails due to whitespace + print("Could not find exact match for Nav Tab insertion. Trying simpler match.") + simple_search = '{% trans "WhatsApp Gateway" %}' + parts = content.split(simple_search) + if len(parts) > 1: + # Reconstruct slightly differently but risky + pass + +# 2. Add Tab Content +if 'id="devices" role="tabpanel"' not in content: + devices_pane = """ + +
+
+
+
{% trans "Connected Devices" %}
+ +
+
+
+ + + + + + + + + + + + + {% for device in devices %} + + + + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Device Name" %}{% trans "Type" %}{% trans "Connection" %}{% trans "IP / Port" %}{% trans "Status" %}{% trans "Actions" %}
{{ device.name }} + {{ device.get_device_type_display }} + {{ device.get_connection_type_display }} + {% if device.ip_address %} + {{ device.ip_address }}{% if device.port %}:{{ device.port }}{% endif %} + {% else %} + - + {% endif %} + + {% if device.is_active %} + {% trans "Active" %} + {% else %} + {% trans "Inactive" %} + {% endif %} + + + +
+
+ + {% trans "No devices configured." %} +
+
+
+
+
+
+ " + + parts = content.split('') + if len(parts) > 1: + last_div = parts[0].rfind('') + second_last_div = parts[0].rfind('', 0, last_div) + + if second_last_div != -1: + new_part0 = parts[0][:second_last_div] + devices_pane + parts[0][second_last_div:] + content = new_part0 + '' + parts[1] + print("Added Devices Tab Pane.") + else: + print("Could not find insertion point for Devices Pane.") + +# 3. Add Add Device Modal +if 'id="addDeviceModal"' not in content: + modal_content = """ + + +" + content = content.replace('{% endblock %}', modal_content + '\n{% endblock %}') + print("Added Add Device Modal.") + +with open(file_path, 'w') as f: + f.write(content) \ No newline at end of file diff --git a/patch_views.py b/patch_views.py new file mode 100644 index 0000000..7bd7fcf --- /dev/null +++ b/patch_views.py @@ -0,0 +1,85 @@ +import re + +file_path = 'core/views.py' + +with open(file_path, 'r') as f: + content = f.read() + +# 1. Add Device to imports +if 'Device' not in content: + pattern = r'(from \.models import \(.*?)(\))' + replacement = r'\1, Device\2' + content = re.sub(pattern, replacement, content, flags=re.DOTALL) + print("Added Device to imports.") + +# 2. Update settings_view +if 'devices = Device.objects.all()' not in content: + # Find the lines before context creation + search_str = 'loyalty_tiers = LoyaltyTier.objects.all().order_by("min_points")' + insert_str = '\n devices = Device.objects.all().order_by("name")' + content = content.replace(search_str, search_str + insert_str) + + # Update context + context_search = '"loyalty_tiers": loyalty_tiers' + context_insert = ',\n "devices": devices' + content = content.replace(context_search, context_search + context_insert) + print("Updated settings_view.") + +# 3. Add Device views +new_views = """ + +@login_required +def add_device(request): + if request.method == 'POST': + name = request.POST.get('name') + device_type = request.POST.get('device_type') + connection_type = request.POST.get('connection_type') + ip_address = request.POST.get('ip_address') + port = request.POST.get('port') + is_active = request.POST.get('is_active') == 'on' + + Device.objects.create( + name=name, + device_type=device_type, + connection_type=connection_type, + ip_address=ip_address if ip_address else None, + port=port if port else None, + is_active=is_active + ) + messages.success(request, _("Device added successfully!")) + return redirect(reverse('settings') + '#devices') + +@login_required +def edit_device(request, pk): + device = get_object_or_404(Device, pk=pk) + if request.method == 'POST': + device.name = request.POST.get('name') + device.device_type = request.POST.get('device_type') + device.connection_type = request.POST.get('connection_type') + device.ip_address = request.POST.get('ip_address') + device.port = request.POST.get('port') + device.is_active = request.POST.get('is_active') == 'on' + + if not device.ip_address: + device.ip_address = None + if not device.port: + device.port = None + + device.save() + messages.success(request, _("Device updated successfully!")) + return redirect(reverse('settings') + '#devices') + +@login_required +def delete_device(request, pk): + device = get_object_or_404(Device, pk=pk) + device.delete() + messages.success(request, _("Device deleted successfully!")) + return redirect(reverse('settings') + '#devices') +""" + +if 'def add_device(request):' not in content: + content += new_views + print("Added Device views.") + +with open(file_path, 'w') as f: + f.write(content) \ No newline at end of file