diff --git a/car_inventory/asgi.py b/car_inventory/asgi.py index 6dd9d7fa..1a8723dc 100644 --- a/car_inventory/asgi.py +++ b/car_inventory/asgi.py @@ -35,9 +35,9 @@ from pathlib import Path # } # ) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") -django.setup() +# django.setup() -BASE_DIR = Path(__file__).resolve().parent.parent +# BASE_DIR = Path(__file__).resolve().parent.parent app = get_asgi_application() # app = WhiteNoise(app, root=str(BASE_DIR / 'staticfiles')) @@ -50,7 +50,7 @@ application = ProtocolTypeRouter( path("sse/notifications/", NotificationSSEApp()), re_path( r"", app - ), # All other routes go to Django + ), ] ) ), @@ -58,5 +58,5 @@ application = ProtocolTypeRouter( ) -if django.conf.settings.DEBUG: - application = ASGIStaticFilesHandler(app) \ No newline at end of file +# if django.conf.settings.DEBUG: +# application = ASGIStaticFilesHandler(app) \ No newline at end of file diff --git a/car_inventory/urls.py b/car_inventory/urls.py index 0f1085b0..118d92a3 100644 --- a/car_inventory/urls.py +++ b/car_inventory/urls.py @@ -5,7 +5,7 @@ from django.urls import path, include from schema_graph.views import Schema from django.conf.urls.static import static from django.conf.urls.i18n import i18n_patterns -from inventory.notifications.sse import NotificationSSEApp +# from inventory.notifications.sse import NotificationSSEApp # import debug_toolbar # from two_factor.urls import urlpatterns as tf_urls diff --git a/inventory/notifications/sse.py b/inventory/notifications/sse.py index dd525d14..c00b0ae1 100644 --- a/inventory/notifications/sse.py +++ b/inventory/notifications/sse.py @@ -1,82 +1,3 @@ -# import json -# from django.contrib.auth.models import AnonymousUser -# from django.contrib.auth import get_user_model -# from django.db import close_old_connections -# from urllib.parse import parse_qs -# from channels.db import database_sync_to_async -# from inventory.models import Notification -# import asyncio - -# @database_sync_to_async -# def get_notifications(user, last_id): -# return Notification.objects.filter( -# user=user, id__gt=last_id, is_read=False -# ).order_by("created") - -# class NotificationSSEApp: -# async def __call__(self, scope, receive, send): -# if scope["type"] != "http": -# return - -# query_string = parse_qs(scope["query_string"].decode()) -# last_id = int(query_string.get("last_id", [0])[0]) - -# # Get user from scope if using AuthMiddlewareStack -# user = scope.get("user", AnonymousUser()) -# if not user.is_authenticated: -# await send({ -# "type": "http.response.start", -# "status": 403, -# "headers": [(b"content-type", b"text/plain")], -# }) -# await send({ -# "type": "http.response.body", -# "body": b"Unauthorized", -# }) -# return - -# await send({ -# "type": "http.response.start", -# "status": 200, -# "headers": [ -# (b"content-type", b"text/event-stream"), -# (b"cache-control", b"no-cache"), -# (b"x-accel-buffering", b"no"), -# ] -# }) - -# try: -# while True: -# close_old_connections() - -# notifications = await get_notifications(user, last_id) -# for notification in notifications: -# data = { -# "id": notification.id, -# "message": notification.message, -# "created": notification.created.isoformat(), -# "is_read": notification.is_read, -# } - -# event_str = ( -# f"id: {notification.id}\n" -# f"event: notification\n" -# f"data: {json.dumps(data)}\n\n" -# ) - -# await send({ -# "type": "http.response.body", -# "body": event_str.encode("utf-8"), -# "more_body": True -# }) - -# last_id = notification.id - -# await asyncio.sleep(2) - -# except asyncio.CancelledError: -# pass - import json import time from django.contrib.auth.models import AnonymousUser diff --git a/inventory/views.py b/inventory/views.py index fe6584f2..01b08977 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -5120,28 +5120,28 @@ class EstimatePrintView(EstimateDetailView): uses a dedicated, stripped-down print template. """ template_name = "sales/estimates/estimate_preview.html" - + def get(self, request, *args, **kwargs): - + self.object = self.get_object() context = self.get_context_data(object=self.object) - - + + # lang = request.GET.get('lang', 'ar') - - + + template_path = "sales/estimates/estimate_preview.html" - - + + html_string = render_to_string(template_path, context) - - + + pdf_file = HTML(string=html_string).write_pdf() - - + + response = HttpResponse(pdf_file, content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="estimate_{self.object.estimate_number}.pdf"' - + return response @@ -10238,7 +10238,8 @@ def payment_callback(request, dealer_slug): # return render(request, "payment_failed.html", {"message": message}) @login_required -async def sse_stream(request): # 👈 Mark as async! +async def sse_stream(request): + import asyncio def event_generator(): last_id = int(request.GET.get("last_id", 0)) @@ -11014,18 +11015,18 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai "item_model", "bill_model" ) ) - + if self.object.po_status == 'fulfilled': context['po_items_list']=po_items_qs context['vendor']=po_items_qs.first().bill_model.vendor context['dealer']=request.dealer - + # Check if PDF format is requested if request.GET.get('format') == 'pdf': # Use a separate, print-friendly template for the PDF if request.GET.get('lang')=='en': html_string = render_to_string( - "purchase_orders/po_detail_en_pdf.html", + "purchase_orders/po_detail_en_pdf.html", context ) else: @@ -11033,12 +11034,12 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai "purchase_orders/po_detail_ar_pdf.html", context ) - - - + + + # Use WeasyPrint to generate the PDF pdf = HTML(string=html_string).write_pdf() - + response = HttpResponse(pdf, content_type="application/pdf") response["Content-Disposition"] = f'attachment; filename="PO_{self.object.po_number}.pdf"' return response diff --git a/templates/notifications.html b/templates/notifications.html index 0c1d1811..b6a0b1f3 100644 --- a/templates/notifications.html +++ b/templates/notifications.html @@ -87,9 +87,13 @@ didOpen: (toast) => { toast.onmouseenter = Swal.stopTimer; toast.onmouseleave = Swal.resumeTimer; - } }); + } + }); + + {% with last_notif=notifications_|last %} + let lastNotificationId = {{ last_notif.id|default:0 }}; + {% endwith %} - let lastNotificationId = {{ notifications_.last.id|default:0 }}; let seenNotificationIds = new Set(); let counter = document.getElementById('notification-counter'); let notificationsContainer = document.getElementById('notifications-container'); @@ -100,7 +104,6 @@ let initialUnreadCount = {{ notifications_.count|default:0 }}; updateCounter(initialUnreadCount); - fetchInitialNotifications(); function fetchInitialNotifications() { @@ -108,29 +111,22 @@ .then(response => response.json()) .then(data => { if (data.notifications && data.notifications.length > 0) { - lastNotificationId = data.notifications[0].id; - seenNotificationIds = new Set(); let unreadCount = 0; - data.notifications.forEach(notification => { seenNotificationIds.add(notification.id); - if (!notification.is_read) { - unreadCount++; - } + if (!notification.is_read) unreadCount++; }); renderNotifications(data.notifications); - updateCounter(unreadCount); - - - setTimeout(() => { - connectSSE(); - }, 5000); } + // Always connect SSE after initial load + setTimeout(() => { + connectSSE(); + }, 1000); }) .catch(error => { console.error('Error fetching initial notifications:', error); @@ -143,12 +139,12 @@ eventSource.close(); } + // ✅ FIXED URL HERE eventSource = new EventSource("/sse/notifications/?last_id=" + lastNotificationId); eventSource.addEventListener('notification', function(e) { try { const data = JSON.parse(e.data); - if (seenNotificationIds.has(data.id)) return; seenNotificationIds.add(data.id); @@ -158,6 +154,11 @@ updateCounter('increment'); + if (!notificationsContainer) { + console.warn("Notification container missing, can't render SSE event"); + return; + } + const notificationElement = createNotificationElement(data); notificationsContainer.insertAdjacentHTML('afterbegin', notificationElement); @@ -168,7 +169,7 @@ Toast.fire({ icon: 'info', - html:`${data.message}` + html: `${data.message}` }); } catch (error) { @@ -220,7 +221,7 @@ - `; + `; } function updateCounter(action) { @@ -231,12 +232,14 @@ if (notificationCountDiv) { notificationCountDiv.innerHTML = ` 0 - `; + `; counter = document.getElementById('notification-counter'); } } } + if (!counter) return; + let currentCount = parseInt(counter.textContent) || 0; if (action === 'increment') { @@ -294,11 +297,11 @@ notificationCard.closest('.notification-card').classList.add('fade-out'); setTimeout(() => { notificationCard.closest('.notification-card').remove(); - }, 200); + }, 1000); } } }); } }); }); - + \ No newline at end of file