Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ApexPOS Restaurent/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';



Expand All @@ -43,6 +45,8 @@ function App() {
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/qrmenu/:tableId" element={<QROrder />} />
<Route path="/kds" element={<KDS />} />

<Route element={<ProtectedRoute />}>
<Route path="/" element={<Layout />}>
Expand Down
139 changes: 139 additions & 0 deletions ApexPOS Restaurent/client/src/pages/KDS.tsx
Original file line number Diff line number Diff line change
@@ -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<Order[]>([]);

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 (
<div className="min-h-screen bg-gray-900 text-white p-6">
<header className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold bg-gradient-to-r from-orange-400 to-red-500 bg-clip-text text-transparent">Kitchen Display System</h1>
<div className="text-gray-400">Live Updates</div>
</header>

<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 items-start">
<AnimatePresence>
{orders.map(order => (
<motion.div
key={order._id}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-gray-800 rounded-xl p-4 shadow-lg border border-gray-700 flex flex-col"
>
<div className="flex justify-between items-center border-b border-gray-700 pb-3 mb-3">
<h2 className="text-xl font-bold bg-gray-700 px-3 py-1 rounded">
Table {order.tableId?.tableNumber || 'N/A'}
</h2>
<span className={`text-sm font-semibold px-2 py-1 rounded ${order.orderType === 'QR-Order' ? 'bg-blue-900 text-blue-300' : 'bg-purple-900 text-purple-300'}`}>
{order.orderType}
</span>
</div>

<div className="flex-1 overflow-y-auto space-y-2 mb-4">
{order.items.map(item => (
<div
key={item._id}
onClick={() => 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'
}`}
>
<div className="flex justify-between font-medium">
<span>{item.quantity}x {item.name}</span>
<span className="text-xs opacity-75 mt-1">{item.status}</span>
</div>
{item.notes && <div className="text-sm text-gray-400 mt-1 flex items-start"><span className="mr-1">💬</span>{item.notes}</div>}
</div>
))}
</div>

<button
onClick={() => updateOrderStatus(order._id, 'Ready')}
className={`w-full py-3 rounded-lg font-bold transition-transform hover:scale-[1.02] active:scale-95 ${
order.status === 'Ready' ? 'bg-green-500 text-white' : 'bg-orange-500 text-white'
}`}
>
{order.status === 'Ready' ? 'Mark Completed' : 'Mark All Ready'}
</button>
</motion.div>
))}
</AnimatePresence>
{orders.length === 0 && (
<div className="col-span-full h-64 flex items-center justify-center text-gray-500 text-2xl font-semibold">
No active orders. Kitchen is clear! 👨‍🍳
</div>
)}
</div>
</div>
);
};

export default KDS;
163 changes: 163 additions & 0 deletions ApexPOS Restaurent/client/src/pages/QROrder.tsx
Original file line number Diff line number Diff line change
@@ -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<any>(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 <div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">Loading Table Data...</div>;

return (
<div className="min-h-screen bg-gray-900 text-white font-sans pb-24">
<header className="bg-gray-800 p-4 sticky top-0 z-10 border-b border-gray-700 text-center">
<h1 className="text-xl font-bold bg-gradient-to-r from-orange-400 to-red-500 bg-clip-text text-transparent">
Table {session.table?.tableNumber}
</h1>
<p className="text-sm text-gray-400">Order from your phone</p>
</header>

<div className="p-4">
{activeTab === 'menu' && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-4">
{menu.products.map(product => (
<div key={product._id} className="bg-gray-800 p-4 rounded-xl flex justify-between items-center outline outline-1 outline-gray-700">
<div>
<h3 className="font-semibold text-lg">{product.englishName}</h3>
<p className="text-orange-400 font-medium">Rs. {product.salesPrice?.toFixed(2)}</p>
</div>
<button
onClick={() => addToCart(product)}
className="bg-orange-500 hover:bg-orange-600 text-white w-10 h-10 rounded-full flex items-center justify-center text-xl font-bold transition-transform active:scale-95"
>
+
</button>
</div>
))}
</motion.div>
)}

{activeTab === 'cart' && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-4">
<h2 className="text-xl font-bold mb-4">Your Cart</h2>
{cart.length === 0 ? (
<p className="text-gray-400 text-center py-10">Cart is empty</p>
) : (
<>
{cart.map((item, idx) => (
<div key={idx} className="bg-gray-800 p-4 rounded-xl flex justify-between items-center outline outline-1 outline-gray-700">
<div>
<h3 className="font-semibold">{item.quantity}x {item.product.englishName}</h3>
<p className="text-gray-400 text-sm">Rs. {(item.product.salesPrice * item.quantity).toFixed(2)}</p>
</div>
</div>
))}
<div className="pt-4 border-t border-gray-700 mt-6 flex justify-between text-xl font-bold">
<span>Total:</span>
<span className="text-orange-400">
Rs. {cart.reduce((acc, item) => acc + (item.product.salesPrice * item.quantity), 0).toFixed(2)}
</span>
</div>
<button
onClick={placeOrder}
className="w-full bg-gradient-to-r from-orange-500 to-red-500 p-4 rounded-xl font-bold text-lg mt-6 shadow-lg shadow-orange-500/20 active:scale-[0.98]"
>
Confirm & Send to Kitchen
</button>
</>
)}
</motion.div>
)}

{activeTab === 'status' && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center py-20 space-y-4">
<div className="w-24 h-24 bg-orange-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
<span className="text-4xl">👨‍🍳</span>
</div>
<h2 className="text-2xl font-bold text-orange-400">Order Sent!</h2>
<p className="text-gray-400">The kitchen has received your order and is working its magic.</p>
<p className="text-lg font-semibold mt-4 bg-gray-800 py-3 rounded-lg border border-gray-700">
Current Status: <span className="text-orange-400">{orderStatus}</span>
</p>
</motion.div>
)}
</div>

{/* Bottom Nav */}
<nav className="fixed bottom-0 left-0 right-0 bg-gray-800 border-t border-gray-700 p-4 flex justify-around">
<button
onClick={() => setActiveTab('menu')}
className={`flex flex-col items-center ${activeTab === 'menu' ? 'text-orange-400' : 'text-gray-400'}`}
>
<span className="text-xl mb-1">📖</span>
<span className="text-xs font-medium">Menu</span>
</button>
<button
onClick={() => setActiveTab('cart')}
className={`flex flex-col items-center relative ${activeTab === 'cart' ? 'text-orange-400' : 'text-gray-400'}`}
>
<span className="text-xl mb-1">🛒</span>
<span className="text-xs font-medium">Cart</span>
{cart.length > 0 && (
<span className="absolute -top-2 -right-2 bg-red-500 text-white text-[10px] font-bold w-5 h-5 rounded-full flex items-center justify-center">
{cart.length}
</span>
)}
</button>
</nav>
</div>
);
};

export default QROrder;
Loading
Loading