diff --git a/pavilion/asgi.py b/pavilion/asgi.py index 8105d75..2e7f64a 100644 --- a/pavilion/asgi.py +++ b/pavilion/asgi.py @@ -10,7 +10,23 @@ import os from django.core.asgi import get_asgi_application +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.auth import AuthMiddlewareStack os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pavilion.settings') -application = get_asgi_application() +# Initialize Django ASGI application early to ensure the AppRegistry +# is populated before importing code that may import ORM models. +django_asgi_app = get_asgi_application() + +from pos_server.routing import websocket_urlpatterns + +application = ProtocolTypeRouter({ + "http": django_asgi_app, + "websocket": AuthMiddlewareStack( + URLRouter( + websocket_urlpatterns + ) + ), +}) + diff --git a/pavilion/settings.py b/pavilion/settings.py index 0829b8e..4404b46 100644 --- a/pavilion/settings.py +++ b/pavilion/settings.py @@ -97,12 +97,14 @@ # Installed applications INSTALLED_APPS = [ + 'daphne', # ASGI server for Django Channels 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'channels', # WebSocket support 'pos_server.apps.PosServerConfig', # POS system module 'webrtc', # WebRTC communication 'app_switcher', # App switcher page module @@ -296,4 +298,14 @@ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'LOCATION': 'unique-snowflake', # Any unique identifier; it differentiates caches if you have multiple ones } +} + +# Django Channels configuration +ASGI_APPLICATION = 'pavilion.asgi.application' + +# Channel layers for WebSocket communication +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels.layers.InMemoryChannelLayer" + } } \ No newline at end of file diff --git a/pos_server/consumers.py b/pos_server/consumers.py new file mode 100644 index 0000000..528197e --- /dev/null +++ b/pos_server/consumers.py @@ -0,0 +1,195 @@ +""" +WebSocket consumers for real-time order updates. + +This module provides WebSocket consumers that enable real-time communication +between the server and clients for order updates. It replaces the polling-based +approach with a push-based WebSocket approach. +""" + +import json +from datetime import datetime +from channels.generic.websocket import AsyncWebsocketConsumer +from channels.db import database_sync_to_async +from django.utils.timezone import now, localdate +from django.db.models import Q + + +class DateTimeEncoder(json.JSONEncoder): + """Custom JSON encoder that handles datetime objects.""" + def default(self, obj): + if isinstance(obj, datetime): + return obj.isoformat() + return super().default(obj) + + +def json_dumps(data): + """JSON dumps with datetime support.""" + return json.dumps(data, cls=DateTimeEncoder) + + +class OrderConsumer(AsyncWebsocketConsumer): + """ + WebSocket consumer for real-time order updates. + + Clients connect to this consumer to receive real-time notifications + when orders are created, updated, or deleted. + """ + + async def connect(self): + """Handle WebSocket connection.""" + self.room_group_name = 'orders' + + # Join the orders group + await self.channel_layer.group_add( + self.room_group_name, + self.channel_name + ) + + await self.accept() + + # Send initial active orders on connection + orders = await self.get_active_orders() + await self.send(text_data=json_dumps({ + 'type': 'initial_orders', + 'orders': orders + })) + + async def disconnect(self, close_code): + """Handle WebSocket disconnection.""" + # Leave the orders group + await self.channel_layer.group_discard( + self.room_group_name, + self.channel_name + ) + + async def receive(self, text_data): + """Handle incoming WebSocket messages.""" + # Currently, clients don't send messages, but this can be extended + pass + + async def order_update(self, event): + """ + Handle order update events from the channel layer. + + This is called when an order is created, updated, or deleted. + """ + await self.send(text_data=json_dumps({ + 'type': event['update_type'], + 'order': event['order'] + })) + + async def order_delete(self, event): + """ + Handle order delete events from the channel layer. + """ + await self.send(text_data=json_dumps({ + 'type': 'order_deleted', + 'order_id': event['order_id'] + })) + + @database_sync_to_async + def get_active_orders(self): + """ + Get all active orders for today. + + Returns a list of serialized order data. + """ + from .models import Order, OrderDish + from .views import collect_order + + today = localdate() + active_orders = Order.objects.filter( + Q(start_time__lte=now()) & + (Q(kitchen_status=0) | + Q(kitchen_status=1) | + Q(bar_status=0) | + Q(bar_status=1) | + Q(gng_status=0) | + Q(gng_status=1) | + Q(picked_up=False)) + ).filter(timestamp__date=today) + + return [collect_order(order) for order in active_orders] + + +class OrderProgressConsumer(AsyncWebsocketConsumer): + """ + WebSocket consumer for customer-facing order progress screen. + + This consumer provides real-time updates for the order status display + that customers see, showing which orders are in progress and ready. + """ + + async def connect(self): + """Handle WebSocket connection.""" + self.room_group_name = 'order_progress' + + # Join the order progress group + await self.channel_layer.group_add( + self.room_group_name, + self.channel_name + ) + + await self.accept() + + # Send initial orders on connection + orders = await self.get_progress_orders() + await self.send(text_data=json_dumps({ + 'type': 'initial_orders', + 'orders': orders + })) + + async def disconnect(self, close_code): + """Handle WebSocket disconnection.""" + await self.channel_layer.group_discard( + self.room_group_name, + self.channel_name + ) + + async def receive(self, text_data): + """Handle incoming WebSocket messages.""" + pass + + async def order_update(self, event): + """Handle order update events.""" + await self.send(text_data=json_dumps({ + 'type': event['update_type'], + 'order': event['order'] + })) + + async def order_delete(self, event): + """Handle order delete events.""" + await self.send(text_data=json_dumps({ + 'type': 'order_deleted', + 'order_id': event['order_id'] + })) + + @database_sync_to_async + def get_progress_orders(self): + """ + Get orders for the progress display (in progress and ready). + """ + from .models import Order + from .views import collect_order + + today = localdate() + + # Get in progress orders (kitchen_status = 1) + in_progress = Order.objects.filter( + kitchen_status=1, + timestamp__date=today + ) + + # Get ready orders (kitchen_status = 2 and not picked up) + ready = Order.objects.filter( + Q(kitchen_status=2) & Q(picked_up=False), + timestamp__date=today + ) + + in_progress_data = [collect_order(order) for order in in_progress] + ready_data = [collect_order(order) for order in ready] + + return { + 'in_progress': in_progress_data, + 'ready': ready_data + } diff --git a/pos_server/routing.py b/pos_server/routing.py new file mode 100644 index 0000000..8abc445 --- /dev/null +++ b/pos_server/routing.py @@ -0,0 +1,11 @@ +""" +WebSocket URL routing for pos_server app. +""" + +from django.urls import re_path +from . import consumers + +websocket_urlpatterns = [ + re_path(r'ws/orders/$', consumers.OrderConsumer.as_asgi()), + re_path(r'ws/order-progress/$', consumers.OrderProgressConsumer.as_asgi()), +] diff --git a/pos_server/signals.py b/pos_server/signals.py index bc96f5a..a7b629b 100644 --- a/pos_server/signals.py +++ b/pos_server/signals.py @@ -1,7 +1,102 @@ -from django.db.models.signals import post_save, pre_save +from django.db.models.signals import post_save, post_delete, pre_save from django.dispatch import receiver from .models import * from . import globals +from channels.layers import get_channel_layer +from asgiref.sync import async_to_sync + + +def broadcast_order_update(order, update_type='order_updated'): + """ + Broadcast order update to all connected WebSocket clients. + + Args: + order: The Order instance that was updated + update_type: The type of update ('order_created', 'order_updated', 'order_deleted') + """ + from .views import collect_order + + channel_layer = get_channel_layer() + if channel_layer is None: + return + + order_data = collect_order(order) if order else None + + # Broadcast to order marking screen + async_to_sync(channel_layer.group_send)( + 'orders', + { + 'type': 'order_update', + 'update_type': update_type, + 'order': order_data + } + ) + + # Broadcast to order progress screen + async_to_sync(channel_layer.group_send)( + 'order_progress', + { + 'type': 'order_update', + 'update_type': update_type, + 'order': order_data + } + ) + + +def broadcast_order_delete(order_id): + """ + Broadcast order deletion to all connected WebSocket clients. + + Args: + order_id: The ID of the deleted order + """ + channel_layer = get_channel_layer() + if channel_layer is None: + return + + # Broadcast to order marking screen + async_to_sync(channel_layer.group_send)( + 'orders', + { + 'type': 'order_delete', + 'order_id': order_id + } + ) + + # Broadcast to order progress screen + async_to_sync(channel_layer.group_send)( + 'order_progress', + { + 'type': 'order_delete', + 'order_id': order_id + } + ) + + +@receiver(post_save, sender=Order) +def order_saved(sender, instance, created, **kwargs): + """ + Signal handler for when an Order is saved. + Broadcasts the update to all connected WebSocket clients. + """ + update_type = 'order_created' if created else 'order_updated' + broadcast_order_update(instance, update_type) + + +@receiver(pre_save, sender=Order) +def store_order_id_before_delete(sender, instance, **kwargs): + """Store the order ID before deletion for broadcasting.""" + pass # pre_save is not needed for deletion + + +@receiver(post_delete, sender=Order) +def order_deleted(sender, instance, **kwargs): + """ + Signal handler for when an Order is deleted. + Broadcasts the deletion to all connected WebSocket clients. + """ + broadcast_order_delete(instance.id) + # This is a corpse, maybe will be needed in the future. diff --git a/pos_server/static/pos_server/kitchen-bar.js b/pos_server/static/pos_server/kitchen-bar.js index d499521..c34d108 100644 --- a/pos_server/static/pos_server/kitchen-bar.js +++ b/pos_server/static/pos_server/kitchen-bar.js @@ -1,82 +1,592 @@ -const pollEverySecs = 11; // Put something that's not a factor of 10 to avoid race condition with auto actions. Needs to be fixed later. -const pollEveryMilisecs = pollEverySecs * 1000; +// WebSocket connection for real-time order updates +let ordersSocket = null; +let reconnectAttempts = 0; +const maxReconnectAttempts = 10; +const reconnectDelay = 3000; const rejectOrderDialog = document.querySelector("#reject-reason") const orderManagementDialog = document.querySelector("#order-management") const preferencesDialog = document.querySelector("#filters-dialog") -document.addEventListener("DOMContentLoaded", () => { - let prevOrderId; +// Module-level variables that will be initialized in DOMContentLoaded +let mainDiv; +let cards = []; +let selectedIndex = 0; +let ordersState = []; +let csrftoken = ''; +let freezeDeletion = false; +let availabilityDialog; +let autoDoneTimeout; +let autoCollectTimeout; + +function connectWebSocket() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ws/orders/`; + + ordersSocket = new WebSocket(wsUrl); + + ordersSocket.onopen = function(e) { + console.log('WebSocket connected for orders'); + reconnectAttempts = 0; + }; + + ordersSocket.onmessage = function(e) { + const data = JSON.parse(e.data); + handleWebSocketMessage(data); + }; + + ordersSocket.onclose = function(e) { + console.log('WebSocket closed. Attempting to reconnect...'); + if (reconnectAttempts < maxReconnectAttempts) { + reconnectAttempts++; + setTimeout(connectWebSocket, reconnectDelay); + } + }; + + ordersSocket.onerror = function(e) { + console.error('WebSocket error:', e); + }; +} + +function handleWebSocketMessage(data) { + if (data.type === 'initial_orders') { + // Initial orders received on connection - filter and update state + const filteredOrders = data.orders.filter(order => shouldShowOrder(order)); + ordersState = filteredOrders; + processInitialOrders(filteredOrders); + } else if (data.type === 'order_created') { + // New order created + if (data.order && shouldShowOrder(data.order)) { + if (!ordersState.some(o => o.order_id === data.order.order_id)) { + ordersState.push(data.order); + appendOrder(data.order); + } + } + } else if (data.type === 'order_updated') { + // Order updated + if (data.order) { + const existingIndex = ordersState.findIndex(o => o.order_id === data.order.order_id); + if (existingIndex !== -1) { + ordersState[existingIndex] = data.order; + updateExistingOrder(data.order); + } else if (shouldShowOrder(data.order)) { + ordersState.push(data.order); + appendOrder(data.order); + } + } + } else if (data.type === 'order_deleted') { + // Order deleted + const orderToRemove = ordersState.find(o => o.order_id === data.order_id); + if (orderToRemove) { + removeOrder(orderToRemove); + ordersState = ordersState.filter(o => o.order_id !== data.order_id); + } + } +} + +function shouldShowOrder(order) { + // Check if order should be shown based on current filters + if (!order || order.picked_up) return false; + + if (orderFilters.kitchen && order.kitchen_status <= 2 && order.kitchen_status !== 4) return true; + if (orderFilters.bar && order.bar_status <= 2 && order.bar_status !== 4) return true; + if (orderFilters.gng && order.gng_status <= 2 && order.gng_status !== 4) return true; + + return false; +} + +function processInitialOrders(orders) { + // Update existing cards and add new ones + orders.forEach(order => { + const existingCard = document.querySelector(`[data-order-id="${order.order_id}"]`); + if (existingCard) { + updateExistingOrder(order); + } + }); +} + +function updateExistingOrder(order) { + const card = cards.find(c => parseInt(c.dataset.orderId) === order.order_id); + if (card) { + card.dataset.kitchenStatus = order.kitchen_status; + card.dataset.barStatus = order.bar_status; + card.dataset.gngStatus = order.gng_status; + updateColors(card); + + // Check if order should be removed (all stations done or picked up) + if (!shouldShowOrder(order)) { + removeOrder(order); + } + } +} + +function appendOrder(data) { + const orderId = data.order_id; + const existingOrder = document.querySelector(`[data-order-id="${orderId}"]`) + + if (existingOrder) { + return; + } + + if (!cards.length) { + mainDiv.innerHTML = ''; + selectedIndex = 0; + } + const newOrder = document.createElement("div"); + newOrder.className = `order ${!cards.length ? "selected" : ""}`; + newOrder.dataset.orderId = orderId; + newOrder.dataset.channel = data.channel; + newOrder.dataset.paymentId = data.payment_id; + newOrder.dataset.kitchenStatus = data.kitchen_status; + newOrder.dataset.barStatus = data.bar_status; + newOrder.dataset.gngStatus = data.gng_status; + let channel; + if (data.channel == "store") { + channel = 'storefront ' + gettext('In-person') + } else if (data.channel == "web") { + channel = `shopping_cart_checkout ` + gettext("Online pick-up") + + `call ${data.phone}` + } else if (data.channel == "delivery") { + channel = `local_shipping ` + gettext("Delivery") + + `call ${data.phone}` + } + const progresses = document.createElement("div"); + progresses.classList.add("progresses") + if (data.kitchen_status != 4) { + const statusStack = document.createElement("div"); + statusStack.classList.add("kitchen-progress", "progress-stack") + statusStack.innerHTML = `restaurant +
+ hourglass_bottom + hourglass_top +
+ check + done_all` + progresses.appendChild(statusStack) + } + if (data.bar_status != 4) { + const statusStack = document.createElement("div"); + statusStack.classList.add("bar-progress", "progress-stack") + statusStack.innerHTML = `local_cafe +
+ hourglass_bottom + hourglass_top +
+ check + done_all` + progresses.appendChild(statusStack) + } + if (data.gng_status != 4) { + const statusStack = document.createElement("div"); + statusStack.classList.add("gng-progress", "progress-stack") + statusStack.innerHTML = `kitchen +
+ hourglass_bottom + hourglass_top +
+ check + done_all` + progresses.appendChild(statusStack) + } + newOrder.innerHTML = `
+

${data.name ? data.name : gettext("No name")}

+
+ Order #${orderId} + + ${gettext("Prep time")}: ${data.start_time} + +
+
+
+

+ ${channel} +

+ ${progresses.outerHTML} +

+ ${data.to_go_order ? "takeout_dining " + gettext("Order to-go") : "restaurant " + gettext("Order for here")} +

+ + ${data.special_instructions ? '

' + gettext("Special instructions") + ':

' + data.special_instructions + '

' : ''} +
` + newOrder.addEventListener("click", e => { + updateSelection(cards.findIndex(cd => cd === e.currentTarget)) + }) + attachSwipability(newOrder) + mainDiv.appendChild(newOrder) + trackTime(newOrder.querySelector(".timestamp")) + const list = document.querySelector(`#order${data.order_id}ul`); + for (const dish of data.dishes) { + if (filters.includes(dish.station)) { + const item = document.createElement("li"); + item.innerHTML = `${dish.quantity} X ${dish.name}`; + list.appendChild(item); + } + } + cards = [...document.querySelectorAll(".order")]; + checkActiveOrders(); +} + +function removeOrder(data) { + const orderId = data.order_id; + const existingOrder = document.querySelector(`[data-order-id="${orderId}"]`) try { - let orders = document.querySelectorAll(".order") - prevOrderId = parseInt(orders[orders.length-1].dataset.orderid) + mainDiv.removeChild(existingOrder) } catch {} + cards = [...document.querySelectorAll(".order")]; + updateSelection(selectedIndex); + checkActiveOrders() +} + +function updateColors(card) { + let progressState; + if (filters.every(filter => ["2", "4"].includes(card.dataset[`${filter}Status`]))) { + progressState = 3; + } else if (pendingApprovalSelf(card)) { + progressState = 0; + } else if (pendingApprovalOtherStations(card)) { + progressState = 1; + } else if (filters.some(filter => ["1"].includes(card.dataset[`${filter}Status`]))) { + progressState = 2; + } + card.dataset.progressState = progressState; +} + +function pendingApprovalSelf(card) { + const stationStatusFields = {}; + stations.forEach(station => { + stationStatusFields[station] = card.dataset[`${station}Status`]; + }); + + // Check stations covered by filters + for (const [station, status] of Object.entries(stationStatusFields)) { + if (filters.includes(station) && status == 0) { // Pending approval + return true; + } + } + + return false; +} + +function pendingApprovalOtherStations(card) { + const stationStatusFields = {}; + stations.forEach(station => { + stationStatusFields[station] = card.dataset[`${station}Status`]; + }); + + // Check stations not covered by filters + for (const [station, status] of Object.entries(stationStatusFields)) { + if (!filters.includes(station) && status == 0) { // Pending approval + return true; + } + } + + return false; +} + +function approveOrder(orderId, approved, rejection = undefined) { + console.log("approve order") + fetch(window.location.href, { + headers:{ + "X-CSRFToken": csrftoken, + "Content-Type": "application/json" + }, + method:'POST', + body: JSON.stringify({ + orderId:orderId, + action: approved ? "approve" : "delete", + filters:filters, + rejection: rejection + }) + }) + .then(response => response.json()) + .then(data => { + console.log(data) + if (data.payment_id && data.all_approved) { + fetch(approveOrderLink, { + headers:{ + "X-CSRFToken": csrftoken, + "Content-Type": "application/json" + }, + method:data.action === "delete" ? 'DELETE' : 'POST', + body: JSON.stringify({ + payment_id:data.payment_id + }) + }) + .then(response => { + if (response.ok) { + processOrderResponse(data); + } + }) + } else { + processOrderResponse(data); + } + if (cards[selectedIndex]) { + cards[selectedIndex].querySelector(".timestamp").setAttribute("data-last-interaction", new Date().toISOString()) + } + }) + + function processOrderResponse(data) { + if (data.action === "delete") { + if (cards[selectedIndex]) { + cards[selectedIndex].classList.add("disappear"); + setTimeout(() => { + try { + document.querySelector("#markings").removeChild(cards[selectedIndex]); + } catch {} + cards = [...document.querySelectorAll(".order")]; + selectedIndex = 0; + updateSelection(selectedIndex); + freezeDeletion = false; + checkActiveOrders(); + }, 400); + } + } else if (data.action === "approve") { + if (cards[selectedIndex]) { + filters.forEach(filter => {cards[selectedIndex].dataset[`${filter}Status`] = 1}) + } + } + checkActiveOrders() + if (cards[selectedIndex]) { + updateColors(cards[selectedIndex]) + } + } +} + +function markOrderDone(orderId) { + const card = cards.find(card => String(card.dataset.orderId) === String(orderId)); + if (!card) return; + + const orderDone = stations.every(filter => ["2", "4"].includes(card.dataset[`${filter}Status`])); + if (orderDone && card.dataset.channel === "delivery") {return} + freezeDeletion = true; + if (orderDone) { + fetch(window.location.href, { + headers:{ + "X-CSRFToken": csrftoken, + "Content-Type": "application/json" + }, + method:'DELETE', + body: JSON.stringify({ + orderId:orderId + }) + }).then(response => { + if (response.ok) { + card.classList.add("disappear"); + setTimeout(() => { + try { + document.querySelector("#markings").removeChild(card) + } catch {} + cards = [...document.querySelectorAll(".order")]; + selectedIndex = 0; + updateSelection(selectedIndex); + freezeDeletion = false; + checkActiveOrders(); + }, 400); + } + }) + } else if ( + !pendingApprovalSelf(card) + && + filters.every(filter => ["1", "4"].includes(card.dataset[`${filter}Status`])) + && + !pendingApprovalOtherStations(card) + ) { + fetch(window.location.href, { + headers:{ + "X-CSRFToken": csrftoken, + "Content-Type": "application/json" + }, + method:'PUT', + body: JSON.stringify({ + orderId:orderId, + filters:filters + }) + }).then(response => { + if (response.ok) { + filters.forEach(filter => {card.dataset[`${filter}Status`] = 2}) + setTimeout(() => { + freezeDeletion = false; + }, 400); + updateColors(card) + } + }) + } else { + const icons = shakeOthers(card) + setTimeout(() => { + freezeDeletion = false; + icons.forEach(icon => {icon.classList.remove("shake")}) + }, 2000); + } + card.querySelector(".timestamp").setAttribute("data-last-interaction", new Date().toISOString()) +} + +function shakeOthers(orderCard){ + const icons = [] + const otherFilters = ["kitchen", "bar", "gng"].filter(n => !filters.includes(n)) + otherFilters.forEach(filter => { + const icon = orderCard.querySelector(`.progress-stack.${filter}-progress span:first-child`) + if (icon) { + icons.push(icon) + } + }) + icons.forEach(icon => {icon.classList.add("shake")}) + return icons +} + +function updateSelection(newIndex) { + if (newIndex < cards.length && newIndex >= 0) { + if (cards[selectedIndex]) { + cards[selectedIndex].classList.remove('selected'); + } + cards[newIndex].classList.add('selected'); + const node = document.querySelector(".selected") + if (node) { + node.scrollIntoView({ behavior: 'smooth' }); + } + selectedIndex = newIndex; + } +} + +function trackTime(node) { + var startTime = new Date(node.getAttribute('data-timestamp')); + + function updateCounter() { + var now = new Date(); + var differenceInSeconds = Math.floor((now - startTime) / 1000); + + var minutes = Math.floor(differenceInSeconds / 60); + var seconds = differenceInSeconds % 60; + + // Formatting minutes and seconds to always have two digits + minutes = minutes.toString().padStart(2, '0'); + seconds = seconds.toString().padStart(2, '0'); + + if (node.children[0]) { + node.children[0].innerHTML = minutes + ':' + seconds; + } + } + + setInterval(updateCounter, 1000); +} + +function checkActiveOrders() { + if (!mainDiv) return; + if (!mainDiv.querySelector(".order") && !mainDiv.querySelector("h1")) { + const noOrderSign = document.createElement("h1"); + noOrderSign.textContent = gettext("No new orders"); + noOrderSign.style = "text-align: center;" + mainDiv.appendChild(noOrderSign); + } +} + +function attachSwipability(card) { + let startX; + + function handleStart(e) { + if (card.classList.contains('selected')) { + startX = e.touches ? e.touches[0].clientX : e.clientX; + card.style.transition = 'none'; // Disable transition during swipe + } + } + + function handleMove(e) { + if (card.classList.contains('selected')) { + const currentX = e.touches ? e.touches[0].clientX : e.clientX; + const deltaX = currentX - startX; + + // Move the element on the screen, but limit it to half the screen width + const maxDeltaX = window.innerWidth / 2; + card.style.transform = `translateX(${Math.max(-maxDeltaX, Math.min(deltaX, maxDeltaX))}px)`; + } + } + + function handleEnd() { + if (card.classList.contains('selected')) { + const currentX = parseFloat(card.style.transform.replace('translateX(', '').replace('px)', '')); + const maxDeltaX = window.innerWidth / 2; + if (Math.abs(currentX) > maxDeltaX / 2) { + openOrderManagement(); + } + card.style.transition = 'transform 0.5s ease-in-out'; + card.style.transform = 'translateX(0px)'; // Bring it back to its original position + } + } + + card.addEventListener('touchstart', handleStart); + card.addEventListener('touchmove', handleMove); + card.addEventListener('touchend', handleEnd); +} + +function openOrderManagement() { + if (!cards[selectedIndex]) return; + + const orderManagementApproveButton = document.querySelector("#approve-order-button"); + const orderManagementRejectButton = document.querySelector("#reject-order-button"); + const orderManagementDoneButton = document.querySelector("#mark-order-done-button"); + const orderManagementCollectedButton = document.querySelector("#mark-order-collected-button"); + + const state = parseInt(cards[selectedIndex].dataset.progressState); + if (state === 0) { + orderManagementApproveButton.disabled = false; + orderManagementRejectButton.disabled = false; + orderManagementDoneButton.disabled = true; + orderManagementCollectedButton.disabled = true; + } else if (state === 1) { + orderManagementApproveButton.disabled = true; + orderManagementRejectButton.disabled = true; + orderManagementDoneButton.disabled = true; + orderManagementCollectedButton.disabled = true; + } else if (state === 2) { + orderManagementApproveButton.disabled = true; + orderManagementRejectButton.disabled = true; + orderManagementDoneButton.disabled = false; + orderManagementCollectedButton.disabled = true; + } else if (state === 3) { + orderManagementApproveButton.disabled = true; + orderManagementRejectButton.disabled = true; + orderManagementDoneButton.disabled = true; + orderManagementCollectedButton.disabled = false; + } + orderManagementDialog.showModal(); +} + +document.addEventListener("DOMContentLoaded", () => { try {document.querySelector(".order").classList.add("selected");} catch (error) {} const params = new URLSearchParams(document.location.search); - let autoDoneTimeout = params.get("auto-done"); - document.querySelector("#auto-done-input").value = autoDoneTimeout + autoDoneTimeout = params.get("auto-done"); + const autoDoneInput = document.querySelector("#auto-done-input"); + if (autoDoneInput) autoDoneInput.value = autoDoneTimeout; if (autoDoneTimeout) { - autoDoneTimeout = parseInt(autoDoneTimeout) * 60000 + autoDoneTimeout = parseInt(autoDoneTimeout) * 60000; } - let autoCollectTimeout = params.get("auto-collect"); - document.querySelector("#auto-collect-input").value = autoCollectTimeout + autoCollectTimeout = params.get("auto-collect"); + const autoCollectInput = document.querySelector("#auto-collect-input"); + if (autoCollectInput) autoCollectInput.value = autoCollectTimeout; if (autoCollectTimeout) { - autoCollectTimeout = parseInt(autoCollectTimeout) * 60000 + autoCollectTimeout = parseInt(autoCollectTimeout) * 60000; } - const mainDiv = document.querySelector("#markings"); - let cards = document.querySelectorAll(".order"); - cards = [...cards] - let selectedIndex = cards.findIndex(element => element.classList.contains('selected')); - // let isFirstLoad = true; + mainDiv = document.querySelector("#markings"); + cards = [...document.querySelectorAll(".order")]; + selectedIndex = cards.findIndex(element => element.classList.contains('selected')); + if (selectedIndex < 0) selectedIndex = 0; checkActiveOrders(); - let ordersState; - getOrdersFirst(); - async function getOrdersFirst() { - ordersState = await fetchOrders(); - } - - setInterval(async () => { - const newOrders = await fetchOrders(); - // Remove orders that are in ordersState but not in newOrders - if (newOrders.length === 0) { - // If newOrders is empty, remove all orders in ordersState - ordersState.forEach(order => removeOrder(order)); - } else { - // Otherwise, remove orders in ordersState that are not in newOrders - ordersState.forEach(order => { - if (!newOrders.some(item => item.order_id === order.order_id)) { - removeOrder(order); - } - }); - } - // Append new orders to the list - newOrders.forEach(order => { - if (!ordersState.some(item => item.order_id === order.order_id)) { - appendOrder(order); - } - const card = cards.find(card => parseInt(card.dataset.orderId) === order.order_id) - if (card) { - card.dataset.kitchenStatus = order.kitchen_status - card.dataset.barStatus = order.bar_status - card.dataset.gngStatus = order.gng_status - updateColors(cards.find(card => parseInt(card.dataset.orderId) === order.order_id)) - } - }); - // Update the ordersState to match the newOrders - ordersState = newOrders; - }, pollEveryMilisecs); + // Initialize WebSocket connection after DOM is ready + connectWebSocket(); + // Auto-done timer if (autoDoneTimeout) { setInterval(() => { const now = new Date(); cards.forEach(card => { if (card.dataset.progressState == 2) { - console.log(card) - const startTime = new Date(card.querySelector(".timestamp").getAttribute('data-last-interaction') || card.querySelector(".timestamp").getAttribute('data-timestamp')); + const timestampEl = card.querySelector(".timestamp"); + const startTime = new Date(timestampEl.getAttribute('data-last-interaction') || timestampEl.getAttribute('data-timestamp')); if (now - startTime >= autoDoneTimeout) { markOrderDone(card.dataset.orderId); } @@ -85,12 +595,14 @@ document.addEventListener("DOMContentLoaded", () => { }, 60000); } + // Auto-collect timer if (autoCollectTimeout) { setInterval(() => { const now = new Date(); cards.forEach(card => { if (card.dataset.progressState == 3) { - const startTime = new Date(card.querySelector(".timestamp").getAttribute('data-last-interaction') || card.querySelector(".timestamp").getAttribute('data-timestamp')); + const timestampEl = card.querySelector(".timestamp"); + const startTime = new Date(timestampEl.getAttribute('data-last-interaction') || timestampEl.getAttribute('data-timestamp')); if (now - startTime >= autoCollectTimeout) { markOrderDone(card.dataset.orderId); } @@ -98,164 +610,21 @@ document.addEventListener("DOMContentLoaded", () => { }); }, 60000); } - - async function fetchOrders() { - const data = await fetch(checkOrdersLink); - const array = await data.json() - const orders = new UniqueKeySet("order_id") - if (orderFilters.kitchen) { - array.filter(item => (item.kitchen_status <= 2 && item.picked_up === false)).forEach(i => { - orders.add(i) - }); - } - if (orderFilters.bar) { - array.filter(item => (item.bar_status <= 2 && item.picked_up === false)).forEach(i => { - orders.add(i) - });; - } - if (orderFilters.gng) { - array.filter(item => (item.gng_status <= 2 && item.picked_up === false)).forEach(i => { - orders.add(i) - }); - } - return orders.values - } - - function appendOrder(data) { - const orderId = data.order_id; - // if (station === "kitchen" && data.kitchen_done) {return} else if (station === "bar" && data.bar_done) {return} - const existingOrder = document.querySelector(`[data-orderid="${orderId}"]`) - if (existingOrder) {return} - // if (prevOrderId && orderId - 1 !== prevOrderId) { - // window.location.reload() - // } else { - // prevOrderId = orderId; - // } - if (!cards.length) { - mainDiv.innerHTML = ''; - selectedIndex = 0; - } - const newOrder = document.createElement("div"); - newOrder.className = `order ${!cards.length ? "selected" : ""}`; - newOrder.dataset.orderId = orderId; - newOrder.dataset.channel = data.channel; - newOrder.dataset.paymentId = data.payment_id; - newOrder.dataset.kitchenStatus = data.kitchen_status; - newOrder.dataset.barStatus = data.bar_status; - newOrder.dataset.gngStatus = data.gng_status; - let channel; - if (data.channel == "store") { - channel = 'storefront ' + gettext('In-person') - } else if (data.channel == "web") { - channel = `shopping_cart_checkout ` + gettext("Online pick-up") + - `call ${data.phone}` - } else if (data.channel == "delivery") { - channel = `local_shipping ` + gettext("Delivery") + - `call ${data.phone}` - } - const progresses = document.createElement("div"); - progresses.classList.add("progresses") - if (data.kitchen_status != 4) { - const statusStack = document.createElement("div"); - statusStack.classList.add("kitchen-progress", "progress-stack") - statusStack.innerHTML = `restaurant -
- hourglass_bottom - hourglass_top -
- check - done_all` - progresses.appendChild(statusStack) - } - if (data.bar_status != 4) { - const statusStack = document.createElement("div"); - statusStack.classList.add("bar-progress", "progress-stack") - statusStack.innerHTML = `local_cafe -
- hourglass_bottom - hourglass_top -
- check - done_all` - progresses.appendChild(statusStack) - } - if (data.gng_status != 4) { - const statusStack = document.createElement("div"); - statusStack.classList.add("gng-progress", "progress-stack") - statusStack.innerHTML = `kitchen -
- hourglass_bottom - hourglass_top -
- check - done_all` - progresses.appendChild(statusStack) - } - newOrder.innerHTML = `
-

${data.name ? data.name : gettext("No name")}

-
- Order #${orderId} - - ${gettext("Prep time")}: ${data.start_time} - -
-
-
-

- ${channel} -

- ${progresses.outerHTML} -

- ${data.to_go_order ? "takeout_dining " + gettext("Order to-go") : "restaurant " + gettext("Order for here")} -

- - ${data.special_instructions ? '

' + gettext("Special instructions") + ':

' + data.special_instructions + '

' : ''} -
` - newOrder.addEventListener("click", e => { - updateSelection(cards.findIndex(cd => cd === e.currentTarget)) - }) - attachSwipability(newOrder) - mainDiv.appendChild(newOrder) - trackTime(newOrder.querySelector(".timestamp")) - const list = document.querySelector(`#order${data.order_id}ul`); - for (const dish of data.dishes) { - if (filters.includes(dish.station)) { - const item = document.createElement("li"); - item.innerHTML = `${dish.quantity} X ${dish.name}`; - list.appendChild(item); - } - } - cards = document.querySelectorAll(".order"); - cards = [...cards] - }; - - function removeOrder(data) { - const orderId = data.order_id; - const existingOrder = document.querySelector(`[data-order-id="${orderId}"]`) - try { - mainDiv.removeChild(existingOrder) - } catch {} - cards = document.querySelectorAll(".order"); - cards = [...cards] - updateSelection(selectedIndex); - checkActiveOrders() - }; cards.forEach(card => { card.addEventListener("click", e => { updateSelection(cards.findIndex(cd => cd === e.currentTarget)) }) updateColors(card) + attachSwipability(card) }) document.querySelectorAll(".timestamp").forEach(node => trackTime(node)) const csrfTokenInput = document.querySelector('input[name="csrfmiddlewaretoken"]'); - const csrftoken = csrfTokenInput ? csrfTokenInput.value : ''; - let freezeDeletion = false; - const availabilityDialog = document.querySelector("#availability"); + csrftoken = csrfTokenInput ? csrfTokenInput.value : ''; + availabilityDialog = document.querySelector("#availability"); + window.addEventListener('keydown', (e) => { if (!rejectOrderDialog.open && !preferencesDialog.open) { // Normal key bindings if (e.key === 'ArrowDown' || e.key == "2") { @@ -263,9 +632,9 @@ document.addEventListener("DOMContentLoaded", () => { } else if (e.key === 'ArrowUp' || e.key === "8") { updateSelection(selectedIndex - 1); } else if ((e.key === "Enter" || e.key === "5") && !freezeDeletion) { - if (filters.every(filter => ["1", "2", "4"].includes(cards[selectedIndex].dataset[`${filter}Status`]))) { + if (cards[selectedIndex] && filters.every(filter => ["1", "2", "4"].includes(cards[selectedIndex].dataset[`${filter}Status`]))) { markOrderDone(cards[selectedIndex].dataset.orderId) - } else if (pendingApprovalSelf(cards[selectedIndex])) { + } else if (cards[selectedIndex] && pendingApprovalSelf(cards[selectedIndex])) { approveOrder(cards[selectedIndex].dataset.orderId, true) } } else if (e.key === "Backspace" || e.key === "Delete") { @@ -293,19 +662,6 @@ document.addEventListener("DOMContentLoaded", () => { } }); - function shakeOthers(orderCard){ - const icons = [] - const otherFilters = ["kitchen", "bar", "gng"].filter(n => !filters.includes(n)) - otherFilters.forEach(filter => { - const icon = orderCard.querySelector(`.progress-stack.${filter}-progress span:first-child`) - if (icon) { - icons.push(icon) - } - }) - icons.forEach(icon => {icon.classList.add("shake")}) - return icons - } - rejectOrderDialog.querySelector("form").addEventListener("submit", e => { e.preventDefault(); const checkboxes = rejectOrderDialog.querySelectorAll("input[type='checkbox']"); @@ -319,280 +675,10 @@ document.addEventListener("DOMContentLoaded", () => { reasons: [...document.querySelectorAll("#reject-reason input[type='checkbox']:checked")].map(checkbox => checkbox.value), reasonExtra: document.querySelector("#reject-reason textarea").value } - console.log(rejection) approveOrder(cards[selectedIndex].dataset.orderId, false, rejection) rejectOrderDialog.close() }) - function approveOrder(orderId, approved, rejection = undefined) { - console.log("approve order") - fetch(window.location.href, { - headers:{ - "X-CSRFToken": csrftoken, - "Content-Type": "application/json" - }, - method:'POST', - body: JSON.stringify({ - orderId:orderId, - action: approved ? "approve" : "delete", - filters:filters, - rejection: rejection - }) - }) - .then(response => response.json()) - .then(data => { - console.log(data) - if (data.payment_id && data.all_approved) { - fetch(approveOrderLink, { - headers:{ - "X-CSRFToken": csrftoken, - "Content-Type": "application/json" - }, - method:data.action === "delete" ? 'DELETE' : 'POST', - body: JSON.stringify({ - payment_id:data.payment_id - }) - }) - .then(response => { - if (response.ok) { - processOrderResponse(data); - } - }) - } else { - processOrderResponse(data); - } - cards[selectedIndex].querySelector(".timestamp").setAttribute("data-last-interaction", new Date().toISOString()) - }) - - function processOrderResponse(data) { - if (data.action === "delete") { - cards[selectedIndex].classList.add("disappear"); - setTimeout(() => { - document.querySelector("#markings").removeChild(cards[selectedIndex]); - cards = document.querySelectorAll(".order"); - cards = [...cards]; - selectedIndex = 0; - updateSelection(selectedIndex); - freezeDeletion = false; - checkActiveOrders(); - }, 400); - } else if (data.action === "approve") { - filters.forEach(filter => {cards[selectedIndex].dataset[`${filter}Status`] = 1}) - // if (orderFilters.kitchen) { - // cards[selectedIndex].dataset["kitchenStatus"] = 1; - // } - // if (orderFilters.bar) { - // cards[selectedIndex].dataset["barStatus"] = 1; - // } - // if (orderFilters.gng) { - // cards[selectedIndex].dataset["gngStatus"] = 1; - // } - } - checkActiveOrders() - updateColors(cards[selectedIndex]) - } - } - - function updateColors(card) { - let progressState; - if (filters.every(filter => ["2", "4"].includes(card.dataset[`${filter}Status`]))) { - progressState = 3; - // card.classList.add("success") - } else if (pendingApprovalSelf(card)) { - progressState = 0; - } else if (pendingApprovalOtherStations(card)) { - progressState = 1; - } else if (filters.some(filter => ["1"].includes(card.dataset[`${filter}Status`]))) { - progressState = 2; - } - card.dataset.progressState = progressState; - // const order = { - // kitchen_status: parseInt(card.dataset.kitchenStatus), - // bar_status: parseInt(card.dataset.barStatus), - // gng_status: parseInt(card.dataset.gngStatus) - // } - // card.dataset.pendingHere = pendingApprovalSelf(order) - // card.dataset.pendingOthers = pendingApprovalOtherStations(order) - } - - function pendingApprovalSelf(card) { - const stationStatusFields = {}; - stations.forEach(station => { - stationStatusFields[station] = card.dataset[`${station}Status`]; - }); - - // Check stations covered by filters - for (const [station, status] of Object.entries(stationStatusFields)) { - if (filters.includes(station) && status == 0) { // Pending approval - return true; - } - } - - return false; - } - - function pendingApprovalOtherStations(card) { - const stationStatusFields = {}; - stations.forEach(station => { - stationStatusFields[station] = card.dataset[`${station}Status`]; - }); - - // Check stations not covered by filters - for (const [station, status] of Object.entries(stationStatusFields)) { - if (!filters.includes(station) && status == 0) { // Pending approval - return true; - } - } - - return false; - } - - function markOrderDone(orderId) { - const card = cards.find(card => card.dataset.orderId == orderId); - const orderDone = stations.every(filter => ["2", "4"].includes(card.dataset[`${filter}Status`])); - if (orderDone && card.dataset.channel === "delivery") {return} - freezeDeletion = true; - if (orderDone) { - fetch(window.location.href, { - headers:{ - "X-CSRFToken": csrftoken, - "Content-Type": "application/json" - }, - method:'DELETE', - body: JSON.stringify({ - orderId:orderId - }) - }).then(response => { - if (response.ok) { - card.classList.add("disappear"); - setTimeout(() => { - document.querySelector("#markings").removeChild(card) - cards = document.querySelectorAll(".order"); - cards = [...cards] - selectedIndex = 0; - updateSelection(selectedIndex); - freezeDeletion = false; - checkActiveOrders(); - }, 400); - } - }) - } else if ( - !pendingApprovalSelf(card) - && - filters.every(filter => ["1", "4"].includes(card.dataset[`${filter}Status`])) - && - !pendingApprovalOtherStations(card) - ) { - fetch(window.location.href, { - headers:{ - "X-CSRFToken": csrftoken, - "Content-Type": "application/json" - }, - method:'PUT', - body: JSON.stringify({ - orderId:orderId, - filters:filters - }) - }).then(response => { - if (response.ok) { - filters.forEach(filter => {card.dataset[`${filter}Status`] = 2}) - setTimeout(() => { - freezeDeletion = false; - }, 400); - updateColors(card) - } - }) - } else { - const icons = shakeOthers(card) - setTimeout(() => { - freezeDeletion = false; - icons.forEach(icon => {icon.classList.remove("shake")}) - }, 2000); - } - card.querySelector(".timestamp").setAttribute("data-last-interaction", new Date().toISOString()) - } - - function updateSelection(newIndex) { - if (newIndex < cards.length && newIndex >= 0) { - cards[selectedIndex].classList.remove('selected'); - cards[newIndex].classList.add('selected'); - const node = document.querySelector(".selected") - node.scrollIntoView({ behavior: 'smooth' }); - selectedIndex = newIndex; - } - } - - function trackTime(node) { - var startTime = new Date(node.getAttribute('data-timestamp')); - - function updateCounter() { - var now = new Date(); - var differenceInSeconds = Math.floor((now - startTime) / 1000); - - var minutes = Math.floor(differenceInSeconds / 60); - var seconds = differenceInSeconds % 60; - - // Formatting minutes and seconds to always have two digits - minutes = minutes.toString().padStart(2, '0'); - seconds = seconds.toString().padStart(2, '0'); - - node.children[0].innerHTML = minutes + ':' + seconds; - } - - setInterval(updateCounter, 1000); - } - - function checkActiveOrders() { - if (!mainDiv.querySelector(".order") && !mainDiv.querySelector("h1")) { - const noOrderSign = document.createElement("h1"); - noOrderSign.textContent = gettext("No new orders"); - noOrderSign.style = "text-align: center;" - mainDiv.appendChild(noOrderSign); - } - } - - // Functions to swipe away orders - - cards.forEach(card => { attachSwipability(card) }) - - function attachSwipability(card) { - let startX; - - function handleStart(e) { - if (card.classList.contains('selected')) { - startX = e.touches ? e.touches[0].clientX : e.clientX; - card.style.transition = 'none'; // Disable transition during swipe - } - } - - function handleMove(e) { - if (card.classList.contains('selected')) { - const currentX = e.touches ? e.touches[0].clientX : e.clientX; - const deltaX = currentX - startX; - - // Move the element on the screen, but limit it to half the screen width - const maxDeltaX = window.innerWidth / 2; - card.style.transform = `translateX(${Math.max(-maxDeltaX, Math.min(deltaX, maxDeltaX))}px)`; - } - } - - function handleEnd() { - if (card.classList.contains('selected')) { - const currentX = parseFloat(card.style.transform.replace('translateX(', '').replace('px)', '')); - const maxDeltaX = window.innerWidth / 2; - if (Math.abs(currentX) > maxDeltaX / 2) { - openOrderManagement(); - } - card.style.transition = 'transform 0.5s ease-in-out'; - card.style.transform = 'translateX(0px)'; // Bring it back to its original position - } - } - - card.addEventListener('touchstart', handleStart); - card.addEventListener('touchmove', handleMove); - card.addEventListener('touchend', handleEnd); - } - const orderManagementApproveButton = document.querySelector("#approve-order-button") const orderManagementRejectButton = document.querySelector("#reject-order-button") const orderManagementDoneButton = document.querySelector("#mark-order-done-button") @@ -619,32 +705,6 @@ document.addEventListener("DOMContentLoaded", () => { orderManagementDialog.close() }) }) - - function openOrderManagement() { - const state = parseInt(cards[selectedIndex].dataset.progressState); - if (state === 0) { - orderManagementApproveButton.disabled = false; - orderManagementRejectButton.disabled = false; - orderManagementDoneButton.disabled = true; - orderManagementCollectedButton.disabled = true; - } else if (state === 1) { - orderManagementApproveButton.disabled = true; - orderManagementRejectButton.disabled = true; - orderManagementDoneButton.disabled = true; - orderManagementCollectedButton.disabled = true; - } else if (state === 2) { - orderManagementApproveButton.disabled = true; - orderManagementRejectButton.disabled = true; - orderManagementDoneButton.disabled = false; - orderManagementCollectedButton.disabled = true; - } else if (state === 3) { - orderManagementApproveButton.disabled = true; - orderManagementRejectButton.disabled = true; - orderManagementDoneButton.disabled = true; - orderManagementCollectedButton.disabled = false; - } - orderManagementDialog.showModal(); - } }) class UniqueKeySet { @@ -680,4 +740,4 @@ class UniqueKeySet { get values() { return Array.from(this.map.values()); } - } \ No newline at end of file +} diff --git a/pos_server/static/pos_server/orders-status.js b/pos_server/static/pos_server/orders-status.js index 5bb6ee4..f5bf181 100644 --- a/pos_server/static/pos_server/orders-status.js +++ b/pos_server/static/pos_server/orders-status.js @@ -3,8 +3,12 @@ import setClock from "./clock.js"; const inProgressCol = document.querySelector("#col-1 div"); const readyCol = document.querySelector("#col-2 div"); -const pollEverySecs = 5; -const pollEveryMilisecs = pollEverySecs * 1000; + +// WebSocket connection for real-time order updates +let ordersSocket = null; +let reconnectAttempts = 0; +const maxReconnectAttempts = 10; +const reconnectDelay = 3000; const namedAnnouncements = [ "${name}, your order is ready to go!", @@ -47,42 +51,154 @@ getLocation(); getWeather(window.sessionStorage.getItem("latitude"), window.sessionStorage.getItem("longitude")); setClock(clock); -let ordersState; -getOrdersFirst(); -async function getOrdersFirst() { - ordersState = await fetchOrders(); - console.log(ordersState); +let ordersState = { + in_progress: [], + ready: [] +}; + +function connectWebSocket() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ws/order-progress/`; + + ordersSocket = new WebSocket(wsUrl); + + ordersSocket.onopen = function(e) { + console.log('WebSocket connected for order progress'); + reconnectAttempts = 0; + }; + + ordersSocket.onmessage = function(e) { + const data = JSON.parse(e.data); + handleWebSocketMessage(data); + }; + + ordersSocket.onclose = function(e) { + console.log('WebSocket closed. Attempting to reconnect...'); + if (reconnectAttempts < maxReconnectAttempts) { + reconnectAttempts++; + setTimeout(connectWebSocket, reconnectDelay); + } + }; + + ordersSocket.onerror = function(e) { + console.error('WebSocket error:', e); + }; } -setInterval(async () => { - const newOrders = await fetchOrders() - if (newOrders.length >= ordersState.length) { - newOrders.forEach(order => { - let existingOrder = ordersState.find(item => item.order_id === order.order_id) - if (!existingOrder) { - appendNewOrder(order); - } else if (existingOrder && existingOrder.kitchen_done !== order.kitchen_done) { - markOrderReady(order); - } else if (existingOrder && existingOrder.picked_up !== order.picked_up) { +function handleWebSocketMessage(data) { + if (data.type === 'initial_orders') { + // Initial orders received on connection + ordersState = data.orders; + processInitialOrders(data.orders); + } else if (data.type === 'order_created') { + // New order created + if (data.order && isKitchenOrder(data.order)) { + handleOrderUpdate(data.order); + } + } else if (data.type === 'order_updated') { + // Order updated + if (data.order) { + handleOrderUpdate(data.order); + } + } else if (data.type === 'order_deleted') { + // Order deleted + removeOrderById(data.order_id); + } +} + +function isKitchenOrder(order) { + // Check if this is an order that requires kitchen work + return order.kitchen_status !== 4; +} + +function processInitialOrders(orders) { + // Clear existing displays + inProgressCol.innerHTML = ''; + readyCol.innerHTML = ''; + // Populate in progress orders + if (orders.in_progress) { + orders.in_progress.forEach(order => { + appendNewOrder(order); + }); + } + + // Populate ready orders + if (orders.ready) { + orders.ready.forEach(order => { + appendReadyOrder(order); + }); + } +} + +function handleOrderUpdate(order) { + if (!isKitchenOrder(order)) return; + + const existingInProgress = ordersState.in_progress?.find(o => o.order_id === order.order_id); + const existingReady = ordersState.ready?.find(o => o.order_id === order.order_id); + + // Determine current state of order + const isInProgress = order.kitchen_status === 1; + const isReady = order.kitchen_status === 2 && !order.picked_up; + + if (order.picked_up) { + // Order picked up - remove from display + removeOrderById(order.order_id); + ordersState.in_progress = ordersState.in_progress?.filter(o => o.order_id !== order.order_id) || []; + ordersState.ready = ordersState.ready?.filter(o => o.order_id !== order.order_id) || []; + } else if (isReady) { + // Order is ready + if (!existingReady) { + if (existingInProgress) { + // Move from in progress to ready + markOrderReady(order); + ordersState.in_progress = ordersState.in_progress?.filter(o => o.order_id !== order.order_id) || []; + } else { + // New ready order + appendReadyOrder(order); } - }) - } else { - ordersState.forEach(oldOrder => { - if (!newOrders.some(item => item.order_id === oldOrder.order_id)) { - removeOrder(oldOrder) - } - }) + ordersState.ready = ordersState.ready || []; + ordersState.ready.push(order); + } + } else if (isInProgress) { + // Order is in progress + if (!existingInProgress) { + appendNewOrder(order); + ordersState.in_progress = ordersState.in_progress || []; + ordersState.in_progress.push(order); + } } - ordersState = newOrders -}, pollEveryMilisecs); +} -async function fetchOrders() { - const data = await fetch(checkOrdersLink); - const array = await data.json() - return array.filter(item => item.kitchen_needed); +function appendReadyOrder(data) { + const existingOrder = document.querySelector(`span[data-order-id="${data.order_id}"]`); + if (existingOrder && readyCol.contains(existingOrder)) return; + + const newOrder = document.createElement("span"); + const text = document.createElement("span"); + text.textContent = data.name ? data.name : `Order #${data.order_id}`; + newOrder.appendChild(text); + newOrder.dataset.orderId = data.order_id; + newOrder.dataset.dishQty = data.dishes?.length || 0; + newOrder.dataset.dish = data.dishes?.[0]?.name || ''; + readyCol.appendChild(newOrder); } +function removeOrderById(orderId) { + const existingOrder = document.querySelector(`span[data-order-id="${orderId}"]`); + if (existingOrder) { + existingOrder.classList.add("remove"); + setTimeout(() => { + try { + existingOrder.parentElement.removeChild(existingOrder); + } catch {} + }, 500); + } +} + +// Initialize WebSocket connection +connectWebSocket(); + function updateWeather(weather) { const icon = document.querySelector("#weather-icon"); const temp = document.querySelector("#temp"); @@ -211,7 +327,7 @@ function generateAnnouncement(order) { } function appendNewOrder(data) { - console.log("recieved") + console.log("received") // console.log(data) const newOrder = document.createElement("span"); const text = document.createElement("span"); @@ -223,12 +339,16 @@ function appendNewOrder(data) { function markOrderReady(data) { const oldOrder = document.querySelector(`span[data-order-id="${data.order_id}"]`); - if (data.done) { + if (!oldOrder) return; + + if (data.done || data.picked_up) { try { console.log("finished") oldOrder.classList.add("remove"); setTimeout(() => { - readyCol.removeChild(oldOrder) + try { + oldOrder.parentElement.removeChild(oldOrder); + } catch {} }, 500); return } catch (error) {} @@ -239,23 +359,26 @@ function markOrderReady(data) { text.textContent = data.name ? data.name : `Order #${data.order_id}`; newOrder.appendChild(text) newOrder.dataset.orderId = data.order_id; - newOrder.dataset.dishQty = data.dishes.length; - newOrder.dataset.dish = data.dishes[0].name; - newOrder.dataset.barDone === data.bar_done + newOrder.dataset.dishQty = data.dishes?.length || 0; + newOrder.dataset.dish = data.dishes?.[0]?.name || ''; oldOrder.classList.add("remove"); setTimeout(() => { - inProgressCol.removeChild(oldOrder) + try { + inProgressCol.removeChild(oldOrder) + } catch {} }, 500); readyCol.appendChild(newOrder); announceOrderReady(data) } function removeOrder(data) { - if (!data.kitchen_needed) {return} const oldOrder = document.querySelector(`span[data-order-id="${data.order_id}"]`); + if (!oldOrder) return; oldOrder.classList.add("remove"); setTimeout(() => { - readyCol.removeChild(oldOrder) + try { + oldOrder.parentElement.removeChild(oldOrder); + } catch {} }, 500); } diff --git a/requirements.txt b/requirements.txt index ae1e5b1..01d9df8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,6 @@ pydub python-barcode sendgrid pywebpush -whitenoise \ No newline at end of file +whitenoise +channels +daphne \ No newline at end of file