-
Notifications
You must be signed in to change notification settings - Fork 128
feat(orders): Live Kitchen Display System (KDS) View (#175) #200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
anshika1179
wants to merge
5
commits into
rdodiya:gssoc_develop
Choose a base branch
from
anshika1179:feat/live-kds-orders-175
base: gssoc_develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
f7fdc8f
feat(orders): Live Kitchen Display System (KDS) View (#175)
a5adb69
Merge branch 'gssoc_develop' into feat/live-kds-orders-175
anshika1179 725be2e
Potential fix for pull request finding
anshika1179 7ec8d51
Merge branch 'gssoc_develop' into feat/live-kds-orders-175
anshika1179 7c412d8
fix: add missing PaymentService to OrderController
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
88 changes: 88 additions & 0 deletions
88
RestroHub-FrontEnd/src/components/admin/kds/KanbanBoard.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import React from 'react'; | ||
| import OrderCard from './OrderCard'; | ||
|
|
||
| const KanbanBoard = ({ orders, onStatusUpdate }) => { | ||
| // Group orders by status | ||
| const pendingOrders = orders.filter(o => o.status === 'PENDING' || o.status === 'CONFIRMED'); | ||
| const preparingOrders = orders.filter(o => o.status === 'PREPARING'); | ||
| const readyOrders = orders.filter(o => o.status === 'READY'); | ||
|
|
||
| return ( | ||
| <div className="grid grid-cols-1 md:grid-cols-3 gap-6 h-full overflow-hidden"> | ||
| {/* Pending Column */} | ||
| <div className="flex flex-col h-full bg-gray-50 rounded-xl p-4 border border-gray-200"> | ||
| <div className="flex justify-between items-center mb-4 pb-2 border-b-2 border-gray-300"> | ||
| <h2 className="text-xl font-black text-gray-700 uppercase tracking-wider">Pending</h2> | ||
| <span className="bg-gray-200 text-gray-800 py-1 px-3 rounded-full font-bold text-sm"> | ||
| {pendingOrders.length} | ||
| </span> | ||
| </div> | ||
| <div className="flex-1 overflow-y-auto pr-2 space-y-4 pb-20 custom-scrollbar"> | ||
| {pendingOrders.map(order => ( | ||
| <OrderCard key={order.orderId} order={order} onStatusUpdate={onStatusUpdate} /> | ||
| ))} | ||
| {pendingOrders.length === 0 && ( | ||
| <div className="h-32 flex items-center justify-center text-gray-400 font-medium"> | ||
| No pending orders | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Preparing Column */} | ||
| <div className="flex flex-col h-full bg-blue-50 rounded-xl p-4 border border-blue-200"> | ||
| <div className="flex justify-between items-center mb-4 pb-2 border-b-2 border-blue-300"> | ||
| <h2 className="text-xl font-black text-blue-800 uppercase tracking-wider">Preparing</h2> | ||
| <span className="bg-blue-200 text-blue-900 py-1 px-3 rounded-full font-bold text-sm"> | ||
| {preparingOrders.length} | ||
| </span> | ||
| </div> | ||
| <div className="flex-1 overflow-y-auto pr-2 space-y-4 pb-20 custom-scrollbar"> | ||
| {preparingOrders.map(order => ( | ||
| <OrderCard key={order.orderId} order={order} onStatusUpdate={onStatusUpdate} /> | ||
| ))} | ||
| {preparingOrders.length === 0 && ( | ||
| <div className="h-32 flex items-center justify-center text-blue-300 font-medium"> | ||
| No orders preparing | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Ready Column */} | ||
| <div className="flex flex-col h-full bg-green-50 rounded-xl p-4 border border-green-200"> | ||
| <div className="flex justify-between items-center mb-4 pb-2 border-b-2 border-green-300"> | ||
| <h2 className="text-xl font-black text-green-800 uppercase tracking-wider">Ready</h2> | ||
| <span className="bg-green-200 text-green-900 py-1 px-3 rounded-full font-bold text-sm"> | ||
| {readyOrders.length} | ||
| </span> | ||
| </div> | ||
| <div className="flex-1 overflow-y-auto pr-2 space-y-4 pb-20 custom-scrollbar"> | ||
| {readyOrders.map(order => ( | ||
| <OrderCard key={order.orderId} order={order} onStatusUpdate={onStatusUpdate} /> | ||
| ))} | ||
| {readyOrders.length === 0 && ( | ||
| <div className="h-32 flex items-center justify-center text-green-300 font-medium"> | ||
| No orders ready | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
|
|
||
| <style>{` | ||
| .custom-scrollbar::-webkit-scrollbar { | ||
| width: 6px; | ||
| } | ||
| .custom-scrollbar::-webkit-scrollbar-track { | ||
| background: transparent; | ||
| } | ||
| .custom-scrollbar::-webkit-scrollbar-thumb { | ||
| background-color: rgba(156, 163, 175, 0.5); | ||
| border-radius: 20px; | ||
| } | ||
| `}</style> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default KanbanBoard; |
180 changes: 180 additions & 0 deletions
180
RestroHub-FrontEnd/src/components/admin/kds/KitchenDisplaySystem.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| import React, { useState, useEffect, useRef } from 'react'; | ||
| import { Stomp } from '@stomp/stompjs'; | ||
| import SockJS from 'sockjs-client'; | ||
| import api from '@services/common/api'; | ||
| import KanbanBoard from './KanbanBoard'; | ||
| import { toast } from 'react-hot-toast'; | ||
|
|
||
| const KitchenDisplaySystem = () => { | ||
| const [orders, setOrders] = useState([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [isConnected, setIsConnected] = useState(false); | ||
| const stompClientRef = useRef(null); | ||
| const audioContextRef = useRef(null); | ||
|
|
||
| // The selected branch ID could be fetched from context or local storage, assuming branch ID 1 for now if not available | ||
| // In a real scenario, this would come from the auth token or selected context | ||
| const branchId = localStorage.getItem("selectedBranchId") || 1; | ||
|
|
||
| // Initialize Web Audio API for chime | ||
| useEffect(() => { | ||
| try { | ||
| const AudioContext = window.AudioContext || window.webkitAudioContext; | ||
| audioContextRef.current = new AudioContext(); | ||
| } catch (e) { | ||
| console.warn("Web Audio API not supported", e); | ||
| } | ||
| return () => { | ||
| if (audioContextRef.current && audioContextRef.current.state !== 'closed') { | ||
| audioContextRef.current.close(); | ||
| } | ||
| }; | ||
| }, []); | ||
|
|
||
| const playChime = () => { | ||
| if (!audioContextRef.current) return; | ||
|
|
||
| // Resume context if suspended (browser autoplay policy) | ||
| if (audioContextRef.current.state === 'suspended') { | ||
| audioContextRef.current.resume(); | ||
| } | ||
|
|
||
| try { | ||
| const oscillator = audioContextRef.current.createOscillator(); | ||
| const gainNode = audioContextRef.current.createGain(); | ||
|
|
||
| oscillator.type = 'sine'; | ||
| oscillator.frequency.setValueAtTime(587.33, audioContextRef.current.currentTime); // D5 | ||
| oscillator.frequency.exponentialRampToValueAtTime(880.00, audioContextRef.current.currentTime + 0.1); // A5 | ||
|
|
||
| gainNode.gain.setValueAtTime(0, audioContextRef.current.currentTime); | ||
| gainNode.gain.linearRampToValueAtTime(0.3, audioContextRef.current.currentTime + 0.05); | ||
| gainNode.gain.exponentialRampToValueAtTime(0.01, audioContextRef.current.currentTime + 1); | ||
|
|
||
| oscillator.connect(gainNode); | ||
| gainNode.connect(audioContextRef.current.destination); | ||
|
|
||
| oscillator.start(audioContextRef.current.currentTime); | ||
| oscillator.stop(audioContextRef.current.currentTime + 1); | ||
| } catch (e) { | ||
| console.error("Audio playback failed", e); | ||
| } | ||
| }; | ||
|
|
||
| const fetchActiveOrders = async () => { | ||
| try { | ||
| setLoading(true); | ||
| const response = await api.get(`/secure/api/v1/orders/branch/${branchId}/active`); | ||
| setOrders(response.data || []); | ||
| } catch (error) { | ||
| console.error("Error fetching orders:", error); | ||
| toast.error("Failed to load active orders"); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| fetchActiveOrders(); | ||
|
|
||
| // Connect WebSocket | ||
| const connectWebSocket = () => { | ||
| const wsUrl = import.meta.env.VITE_WS_URL || "http://localhost:8181/ws"; | ||
| const socket = new SockJS(wsUrl); | ||
| const client = Stomp.over(socket); | ||
|
|
||
| // Disable debug logging in production | ||
| client.debug = () => {}; | ||
|
|
||
| client.connect({}, (frame) => { | ||
| setIsConnected(true); | ||
| console.log('Connected to KDS WebSocket'); | ||
|
|
||
| client.subscribe(`/topic/orders/branch/${branchId}`, (message) => { | ||
| if (message.body) { | ||
| const notification = JSON.parse(message.body); | ||
| handleOrderNotification(notification); | ||
| } | ||
| }); | ||
| }, (error) => { | ||
| console.error('WebSocket error:', error); | ||
| setIsConnected(false); | ||
| // Attempt to reconnect after 5 seconds | ||
| setTimeout(connectWebSocket, 5000); | ||
| }); | ||
|
anshika1179 marked this conversation as resolved.
|
||
|
|
||
| stompClientRef.current = client; | ||
| }; | ||
|
|
||
| connectWebSocket(); | ||
|
|
||
| return () => { | ||
| if (stompClientRef.current) { | ||
| stompClientRef.current.disconnect(); | ||
| } | ||
| }; | ||
| }, [branchId]); | ||
|
|
||
| const handleOrderNotification = (notification) => { | ||
| const { type, order } = notification; | ||
|
|
||
| if (type === 'NEW_ORDER') { | ||
| playChime(); | ||
| toast.success(`New order #${order.orderId} received!`, { icon: '🔔' }); | ||
| setOrders(prev => { | ||
| // Prevent duplicates | ||
| if (prev.some(o => o.orderId === order.orderId)) return prev; | ||
| return [order, ...prev]; | ||
| }); | ||
| } else if (type === 'STATUS_UPDATE') { | ||
| setOrders(prev => prev.map(o => o.orderId === order.orderId ? order : o)); | ||
| } | ||
| }; | ||
|
|
||
| const handleStatusUpdate = (orderId, newStatus) => { | ||
| // Optimistic update | ||
| setOrders(prev => prev.map(o => | ||
| o.orderId === orderId ? { ...o, status: newStatus } : o | ||
| )); | ||
|
|
||
| // In a real application, if the API call fails (handled in OrderCard), | ||
| // it would call a rollback function here or re-fetch. | ||
| // For simplicity, we assume success or rely on the WebSocket broadcast to correct it. | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="flex flex-col h-screen bg-gray-100 p-4 pt-20 lg:pt-4"> | ||
| <div className="flex justify-between items-center mb-4 bg-white p-4 rounded-xl shadow-sm"> | ||
| <div> | ||
| <h1 className="text-2xl font-bold text-gray-800">Kitchen Display System</h1> | ||
| <div className="flex items-center mt-1"> | ||
| <div className={`w-2 h-2 rounded-full mr-2 ${isConnected ? 'bg-green-500' : 'bg-red-500 animate-pulse'}`}></div> | ||
| <span className="text-sm text-gray-500 font-medium"> | ||
| {isConnected ? 'Live Sync Active' : 'Disconnected - Reconnecting...'} | ||
| </span> | ||
| </div> | ||
| </div> | ||
| <div className="flex space-x-3"> | ||
| <button | ||
| onClick={fetchActiveOrders} | ||
| className="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg font-bold transition-colors" | ||
| > | ||
| Refresh | ||
| </button> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="flex-1 overflow-hidden"> | ||
| {loading ? ( | ||
| <div className="flex items-center justify-center h-full"> | ||
| <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div> | ||
| </div> | ||
| ) : ( | ||
| <KanbanBoard orders={orders} onStatusUpdate={handleStatusUpdate} /> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default KitchenDisplaySystem; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.