diff --git a/ApexPOS Restaurent/client/src/App.tsx b/ApexPOS Restaurent/client/src/App.tsx index 04ed4c6..eff37fe 100644 --- a/ApexPOS Restaurent/client/src/App.tsx +++ b/ApexPOS Restaurent/client/src/App.tsx @@ -27,6 +27,8 @@ import Notifications from './pages/Notifications'; import StaffManagement from './pages/StaffManagement'; import Settings from './pages/Settings'; import TableManagement from './pages/TableManagement'; +import QROrder from './pages/QROrder'; +import KDS from './pages/KDS'; @@ -43,6 +45,8 @@ function App() { } /> + } /> + } /> }> }> diff --git a/ApexPOS Restaurent/client/src/pages/KDS.tsx b/ApexPOS Restaurent/client/src/pages/KDS.tsx new file mode 100644 index 0000000..514d8a4 --- /dev/null +++ b/ApexPOS Restaurent/client/src/pages/KDS.tsx @@ -0,0 +1,139 @@ +import React, { useEffect, useState } from 'react'; +import { io } from 'socket.io-client'; +import { motion, AnimatePresence } from 'framer-motion'; + +const socket = io('http://localhost:5000'); + +interface OrderItem { + _id: string; + name: string; + quantity: number; + notes?: string; + status: 'Pending' | 'Sent' | 'Preparing' | 'Ready' | 'Served' | 'Cancelled'; +} + +interface Order { + _id: string; + tableId: { _id: string; tableNumber: string }; + orderType: string; + status: 'Pending' | 'Preparing' | 'Ready' | 'Completed' | 'Cancelled'; + items: OrderItem[]; + createdAt: string; +} + +const KDS = () => { + const [orders, setOrders] = useState([]); + + useEffect(() => { + // Fetch initial KDS orders + fetch('http://localhost:5000/api/qr/kds/orders') + .then(res => res.json()) + .then(data => setOrders(data)) + .catch(err => console.error(err)); + + socket.on('new_kds_order', (newOrder: Order) => { + setOrders(prev => [...prev.filter(o => o._id !== newOrder._id), newOrder]); + }); + + socket.on('kds_order_updated', (updatedOrder: Order) => { + if (updatedOrder.status === 'Completed' || updatedOrder.status === 'Cancelled') { + setOrders(prev => prev.filter(o => o._id !== updatedOrder._id)); + } else { + setOrders(prev => prev.map(o => o._id === updatedOrder._id ? updatedOrder : o)); + } + }); + + return () => { + socket.off('new_kds_order'); + socket.off('kds_order_updated'); + }; + }, []); + + const updateItemStatus = async (orderId: string, itemId: string, currentStatus: string) => { + let newStatus = 'Preparing'; + if (currentStatus === 'Preparing') newStatus = 'Ready'; + if (currentStatus === 'Ready') return; // Cannot revert back from ready for now + + await fetch(`http://localhost:5000/api/qr/kds/orders/${orderId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ itemId, itemStatus: newStatus }) + }); + }; + + const updateOrderStatus = async (orderId: string, status: string) => { + await fetch(`http://localhost:5000/api/qr/kds/orders/${orderId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status }) + }); + }; + + return ( +
+
+

Kitchen Display System

+
Live Updates
+
+ +
+ + {orders.map(order => ( + +
+

+ Table {order.tableId?.tableNumber || 'N/A'} +

+ + {order.orderType} + +
+ +
+ {order.items.map(item => ( +
updateItemStatus(order._id, item._id, item.status)} + className={`p-3 rounded cursor-pointer transition-colors ${ + item.status === 'Ready' ? 'bg-green-900/50 border border-green-500/50' : + item.status === 'Preparing' ? 'bg-yellow-900/50 border border-yellow-500/50' : + 'bg-gray-700 hover:bg-gray-600' + }`} + > +
+ {item.quantity}x {item.name} + {item.status} +
+ {item.notes &&
💬{item.notes}
} +
+ ))} +
+ + +
+ ))} +
+ {orders.length === 0 && ( +
+ No active orders. Kitchen is clear! 👨‍🍳 +
+ )} +
+
+ ); +}; + +export default KDS; diff --git a/ApexPOS Restaurent/client/src/pages/QROrder.tsx b/ApexPOS Restaurent/client/src/pages/QROrder.tsx new file mode 100644 index 0000000..138bf42 --- /dev/null +++ b/ApexPOS Restaurent/client/src/pages/QROrder.tsx @@ -0,0 +1,163 @@ +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { motion } from 'framer-motion'; + +const QROrder = () => { + const { tableId } = useParams(); + const [menu, setMenu] = useState<{ products: any[], categories: any[] }>({ products: [], categories: [] }); + const [session, setSession] = useState<{ table: any, sessionId: string } | null>(null); + const [cart, setCart] = useState<{ product: any, quantity: number, notes: string }[]>([]); + const [activeTab, setActiveTab] = useState('menu'); + const [orderStatus, setOrderStatus] = useState(null); // 'Pending', 'Preparing', 'Ready' + + useEffect(() => { + // Authenticate table and get session + fetch(`http://localhost:5000/api/qr/table/${tableId}/session`, { method: 'POST' }) + .then(res => res.json()) + .then(data => { + if (data.sessionId) setSession(data); + }); + + // Get Menu + fetch('http://localhost:5000/api/qr/menu') + .then(res => res.json()) + .then(data => setMenu(data)); + }, [tableId]); + + const addToCart = (product: any) => { + setCart(prev => { + const existing = prev.find(item => item.product._id === product._id); + if (existing) { + return prev.map(item => item.product._id === product._id ? { ...item, quantity: item.quantity + 1 } : item); + } + return [...prev, { product, quantity: 1, notes: '' }]; + }); + }; + + const placeOrder = async () => { + if (!session || cart.length === 0) return; + + const totalAmount = cart.reduce((acc, item) => acc + (item.product.salesPrice * item.quantity), 0); + const items = cart.map(item => ({ + productId: item.product._id, + name: item.product.englishName, + price: item.product.salesPrice, + quantity: item.quantity, + notes: item.notes + })); + + await fetch(`http://localhost:5000/api/qr/order`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tableId, sessionId: session.sessionId, items, totalAmount }) + }); + + setCart([]); + setOrderStatus('Pending'); + setActiveTab('status'); + }; + + if (!session) return
Loading Table Data...
; + + return ( +
+
+

+ Table {session.table?.tableNumber} +

+

Order from your phone

+
+ +
+ {activeTab === 'menu' && ( + + {menu.products.map(product => ( +
+
+

{product.englishName}

+

Rs. {product.salesPrice?.toFixed(2)}

+
+ +
+ ))} +
+ )} + + {activeTab === 'cart' && ( + +

Your Cart

+ {cart.length === 0 ? ( +

Cart is empty

+ ) : ( + <> + {cart.map((item, idx) => ( +
+
+

{item.quantity}x {item.product.englishName}

+

Rs. {(item.product.salesPrice * item.quantity).toFixed(2)}

+
+
+ ))} +
+ Total: + + Rs. {cart.reduce((acc, item) => acc + (item.product.salesPrice * item.quantity), 0).toFixed(2)} + +
+ + + )} +
+ )} + + {activeTab === 'status' && ( + +
+ 👨‍🍳 +
+

Order Sent!

+

The kitchen has received your order and is working its magic.

+

+ Current Status: {orderStatus} +

+
+ )} +
+ + {/* Bottom Nav */} + +
+ ); +}; + +export default QROrder; diff --git a/ApexPOS Restaurent/server/controllers/qrOrderController.js b/ApexPOS Restaurent/server/controllers/qrOrderController.js new file mode 100644 index 0000000..2a9c9cb --- /dev/null +++ b/ApexPOS Restaurent/server/controllers/qrOrderController.js @@ -0,0 +1,130 @@ +const { Table, Product, Category, Order } = require('../models/AllModels'); +const { v4: uuidv4 } = require('uuid'); // Will use crypto if uuid not available, let's just make a simple ID generator for now. + +const generateSessionId = () => Math.random().toString(36).substring(2, 15); + +// Get menu for customers +exports.getTableMenu = async (req, res) => { + try { + const products = await Product.find({ isActive: true }); + const categories = await Category.find(); + res.status(200).json({ products, categories }); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +// Start or retrieve a session for a table +exports.startTableSession = async (req, res) => { + try { + const { tableId } = req.params; + const table = await Table.findById(tableId); + if (!table) return res.status(404).json({ message: 'Table not found' }); + + if (!table.activeSessionId || table.status === 'Available') { + table.activeSessionId = generateSessionId(); + table.status = 'Occupied'; + await table.save(); + + // Notify POS that table is now occupied + req.app.get('io').emit('table_status_changed', table); + } + + res.status(200).json({ table, sessionId: table.activeSessionId }); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +// Customer places an order via QR +exports.placeOrder = async (req, res) => { + try { + const { tableId, sessionId, items, totalAmount } = req.body; + const table = await Table.findById(tableId); + + if (!table) return res.status(404).json({ message: 'Table not found' }); + if (table.activeSessionId !== sessionId) { + return res.status(403).json({ message: 'Invalid session for this table. Please scan the QR code again.' }); + } + + // Check if there is an open order for this table + let order = await Order.findOne({ tableId, status: { $in: ['Pending', 'Preparing'] } }); + + if (order) { + // Append items to existing order + order.items.push(...items.map(item => ({ ...item, status: 'Pending' }))); + order.totalAmount += totalAmount; + await order.save(); + } else { + // Create new order + order = new Order({ + tableId, + orderType: 'QR-Order', + status: 'Pending', + items: items.map(item => ({ ...item, status: 'Pending' })), + totalAmount, + customerSessionId: sessionId + }); + await order.save(); + table.currentOrder = order._id; + await table.save(); + } + + // Broadcast to KDS and Main POS + req.app.get('io').emit('new_kds_order', order); + + res.status(201).json(order); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +// Get all active orders for KDS +exports.getKDSOrders = async (req, res) => { + try { + const orders = await Order.find({ orderType: { $in: ['QR-Order', 'Dine-In'] }, status: { $in: ['Pending', 'Preparing', 'Ready'] } }) + .populate('tableId', 'tableNumber') + .sort({ createdAt: 1 }); + + res.status(200).json(orders); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +// Update order / item status from KDS +exports.updateKDSStatus = async (req, res) => { + try { + const { orderId } = req.params; + const { status, itemId, itemStatus } = req.body; + + const order = await Order.findById(orderId).populate('tableId', 'tableNumber'); + if (!order) return res.status(404).json({ message: 'Order not found' }); + + if (itemId && itemStatus) { + // Update specific item status + const item = order.items.id(itemId); + if (item) item.status = itemStatus; + + // Check if all items are ready + const allReady = order.items.every(i => i.status === 'Ready' || i.status === 'Served' || i.status === 'Cancelled'); + if (allReady) order.status = 'Ready'; + + } else if (status) { + // Update whole order status + order.status = status; + if (status === 'Ready') { + order.items.forEach(i => i.status = 'Ready'); + } + } + + await order.save(); + + // Broadcast update to KDS and customer phone and POS + req.app.get('io').emit('kds_order_updated', order); + + res.status(200).json(order); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; diff --git a/ApexPOS Restaurent/server/models/AllModels.js b/ApexPOS Restaurent/server/models/AllModels.js index fe9bd94..a69120f 100644 --- a/ApexPOS Restaurent/server/models/AllModels.js +++ b/ApexPOS Restaurent/server/models/AllModels.js @@ -178,23 +178,27 @@ const tableSchema = new mongoose.Schema({ capacity: Number, status: { type: String, enum: ['Available', 'Occupied', 'Reserved', 'Bill Requested'], default: 'Available' }, currentOrder: { type: mongoose.Schema.Types.ObjectId, ref: 'Order' }, + activeSessionId: { type: String }, // To secure QR ordering per seating session branchId: { type: String, default: 'HQ' } }, { timestamps: true }); const orderSchema = new mongoose.Schema({ tableId: { type: mongoose.Schema.Types.ObjectId, ref: 'Table' }, + orderType: { type: String, enum: ['Dine-In', 'Takeaway', 'QR-Order'], default: 'Dine-In' }, + status: { type: String, enum: ['Pending', 'Preparing', 'Ready', 'Completed', 'Cancelled'], default: 'Pending' }, items: [{ productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' }, name: String, price: Number, quantity: Number, - status: { type: String, enum: ['Pending', 'Sent', 'Served', 'Cancelled'], default: 'Pending' }, + status: { type: String, enum: ['Pending', 'Sent', 'Preparing', 'Ready', 'Served', 'Cancelled'], default: 'Pending' }, kotPrinted: { type: Boolean, default: false }, notes: String }], totalAmount: Number, isPaid: { type: Boolean, default: false }, cashierName: String, + customerSessionId: String, // Matches table activeSessionId branchId: { type: String, default: 'HQ' } }, { timestamps: true }); diff --git a/ApexPOS Restaurent/server/routes/qrRoutes.js b/ApexPOS Restaurent/server/routes/qrRoutes.js new file mode 100644 index 0000000..ff98c81 --- /dev/null +++ b/ApexPOS Restaurent/server/routes/qrRoutes.js @@ -0,0 +1,14 @@ +const express = require('express'); +const router = express.Router(); +const qrOrderController = require('../controllers/qrOrderController'); + +// Customer QR Routes +router.get('/menu', qrOrderController.getTableMenu); +router.post('/table/:tableId/session', qrOrderController.startTableSession); +router.post('/order', qrOrderController.placeOrder); + +// KDS Routes +router.get('/kds/orders', qrOrderController.getKDSOrders); +router.patch('/kds/orders/:orderId', qrOrderController.updateKDSStatus); + +module.exports = router; diff --git a/ApexPOS Restaurent/server/server.js b/ApexPOS Restaurent/server/server.js index 6ecfaa9..af03b3a 100644 --- a/ApexPOS Restaurent/server/server.js +++ b/ApexPOS Restaurent/server/server.js @@ -46,6 +46,7 @@ app.use('/api/notifications', require('./routes/notificationRoutes')); app.use('/api/shifts', require('./routes/shiftRoutes')); app.use('/api/settings', require('./routes/settingsRoutes')); app.use('/api/hospitality', hospitalityRoutes); +app.use('/api/qr', require('./routes/qrRoutes'));