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