From 64ef8a34726adc4cdc5de7b68a1817544125d242 Mon Sep 17 00:00:00 2001 From: Alaguraj0361 Date: Thu, 22 Jan 2026 22:19:06 +0530 Subject: [PATCH] restaurant functionality updated --- src/app/api/auth/me/route.ts | 2 +- src/app/pos/page.tsx | 12 +- src/app/pos/pos.module.css | 59 ++- src/app/restaurant/orders/orders.module.css | 472 +++++++++++++++++ src/app/restaurant/orders/page.tsx | 275 ++++++++++ src/app/restaurant/page.tsx | 499 +++++++++++++----- src/app/restaurant/restaurant.module.css | 96 +++- src/components/AppLayout/AppLayout.module.css | 6 +- src/components/AppLayout/AppLayout.tsx | 31 +- 9 files changed, 1275 insertions(+), 177 deletions(-) create mode 100644 src/app/restaurant/orders/orders.module.css create mode 100644 src/app/restaurant/orders/page.tsx diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts index e7917cb..befdcf7 100644 --- a/src/app/api/auth/me/route.ts +++ b/src/app/api/auth/me/route.ts @@ -7,7 +7,7 @@ const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; export async function GET() { try { const cookieStore = await cookies(); - const token = cookieStore.get('token')?.value; + const token = cookieStore.get('auth_token')?.value; if (!token) { return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); diff --git a/src/app/pos/page.tsx b/src/app/pos/page.tsx index 4b69c2a..4d25ea7 100644 --- a/src/app/pos/page.tsx +++ b/src/app/pos/page.tsx @@ -713,13 +713,13 @@ export default function POSPage() { if (step === 'DASHBOARD') { return (
-
+
- + Dashboard - Point of Sale + Point of Sale
@@ -735,7 +735,7 @@ export default function POSPage() {
{session.name}
- {session.isRestaurant ? : } + {session.isRestaurant ? : }
@@ -891,11 +891,11 @@ export default function POSPage() {
- updateTable(selectedTable.id, { name: e.target.value })} /> + updateTable(selectedTable.id, { name: e.target.value })} />
- updateTable(selectedTable.id, { seats: parseInt(e.target.value) || 0 })} /> + updateTable(selectedTable.id, { seats: parseInt(e.target.value) || 0 })} />
diff --git a/src/app/pos/pos.module.css b/src/app/pos/pos.module.css index 4a28439..1f8b565 100644 --- a/src/app/pos/pos.module.css +++ b/src/app/pos/pos.module.css @@ -119,12 +119,12 @@ border: 2px solid transparent; } -/* Dashboard Upgrade */ +/* Dashboard Upgrade - Light Version */ .dashboardContainer { flex: 1; padding: 3rem; overflow-y: auto; - background: radial-gradient(circle at top right, #1e293b 0%, #0b0f19 100%); + background: #f8fafc; display: flex; flex-direction: column; gap: 3rem; @@ -138,7 +138,7 @@ .dashboardTitle { font-size: 2.5rem; font-weight: 800; - background: linear-gradient(to right, #f8fafc, #2dd4bf); + background: linear-gradient(to right, #0f172a, #0d9488); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; @@ -146,7 +146,7 @@ } .dashboardSubtitle { - color: #94a3b8; + color: #64748b; font-size: 1.1rem; } @@ -158,31 +158,50 @@ } .sessionCard { - background: rgba(30, 41, 59, 0.7); - backdrop-filter: blur(12px); + background: #ffffff; border-radius: 28px; padding: 2.5rem; position: relative; - border: 1px solid rgba(255, 255, 255, 0.08); + border: 1px solid #e2e8f0; transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); display: flex; flex-direction: column; min-height: 280px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05); } .sessionCard:hover { transform: translateY(-12px) scale(1.02); - border-color: rgba(45, 212, 191, 0.4); - box-shadow: 0 40px 60px -20px rgba(0, 0, 0, 0.6); + border-color: #2dd4bf; + box-shadow: 0 40px 60px -20px rgba(0, 0, 0, 0.1); } .sessionName { font-size: 1.8rem; font-weight: 800; - color: #f8fafc; + color: #0f172a; letter-spacing: -0.5px; } +.sessionCardHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.5rem; +} + +.sessionIcon { + width: 60px; + height: 60px; + background: #f1f5f9; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + color: #0d9488; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02); +} + .sessionBadges { display: flex; flex-wrap: wrap; @@ -199,19 +218,19 @@ } .badgeInfo { - background: rgba(8, 145, 178, 0.2); - color: #22d3ee; + background: #e0f2fe; + color: #0369a1; } .badgeWarn { - background: rgba(185, 28, 28, 0.2); - color: #f87171; + background: #fee2e2; + color: #b91c1c; } .continueBtn { margin-top: auto; - background: linear-gradient(135deg, #2dd4bf, #14b8a6); - color: #0f172a; + background: linear-gradient(135deg, #0d9488, #0f766e); + color: #ffffff; border: none; padding: 1.2rem 2rem; border-radius: 18px; @@ -219,6 +238,14 @@ font-size: 1.1rem; cursor: pointer; text-align: center; + box-shadow: 0 4px 12px rgba(13, 148, 136, 0.2); + transition: all 0.3s; +} + +.continueBtn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(13, 148, 136, 0.3); + filter: brightness(1.1); } .userInitial { diff --git a/src/app/restaurant/orders/orders.module.css b/src/app/restaurant/orders/orders.module.css new file mode 100644 index 0000000..b3571b2 --- /dev/null +++ b/src/app/restaurant/orders/orders.module.css @@ -0,0 +1,472 @@ +.container { + display: flex; + flex-direction: column; + gap: 1.5rem; + padding-bottom: 2rem; +} + +.header { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.statsRow { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; +} + +.statCard { + background: white; + padding: 1.5rem; + border-radius: 20px; + border: 1px solid #e2e8f0; + display: flex; + flex-direction: column; + gap: 0.5rem; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); +} + +.statLabel { + font-size: 0.85rem; + color: #64748b; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.statValue { + font-size: 1.75rem; + font-weight: 800; + color: #1e293b; +} + +.filterBar { + display: flex; + justify-content: space-between; + align-items: center; + background: white; + padding: 1rem; + border-radius: 16px; + border: 1px solid #e2e8f0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.search { + display: flex; + align-items: center; + gap: 0.75rem; + background: #f1f5f9; + padding: 0.5rem 1rem; + border-radius: 10px; + width: 350px; + color: #64748b; +} + +.search input { + border: none; + background: none; + outline: none; + flex: 1; + font-size: 0.9rem; + color: #1e293b; +} + +.statusFilters { + display: flex; + gap: 0.5rem; + background: #f1f5f9; + padding: 4px; + border-radius: 10px; +} + +.filterBtn { + padding: 0.4rem 1rem; + border: none; + background: none; + font-size: 0.8rem; + font-weight: 700; + border-radius: 8px; + cursor: pointer; + color: #64748b; + transition: all 0.2s ease; +} + +.activeFilter { + background: white; + color: var(--pk-primary); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.tableWrapper { + background: white; + border-radius: 20px; + border: 1px solid #e2e8f0; + overflow: hidden; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); +} + +.ordersTable { + width: 100%; + border-collapse: collapse; +} + +.ordersTable th { + text-align: left; + padding: 1.25rem 1rem; + background: #f8fafc; + border-bottom: 2px solid #e2e8f0; + color: #64748b; + font-size: 0.8rem; + text-transform: uppercase; + font-weight: 700; +} + +.ordersTable td { + padding: 1.25rem 1rem; + border-bottom: 1px solid #f1f5f9; + font-size: 0.95rem; + color: #1e293b; +} + +.orderId { + font-weight: 800; + color: var(--pk-primary); +} + +.tableBadge { + background: #eff6ff; + color: #1d4ed8; + padding: 0.25rem 0.6rem; + border-radius: 6px; + font-weight: 700; + font-size: 0.8rem; +} + +.timeCell { + display: flex; + align-items: center; + gap: 0.5rem; + color: #64748b; +} + +.totalCell { + font-weight: 700; + color: #0f172a; +} + +.statusBadge { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.3rem 0.75rem; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; +} + +.paid { + background: #dcfce7; + color: #15803d; +} + +.pending, +.ordered { + background: #fef3c7; + color: #b45309; +} + +.viewBtn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-size: 0.85rem; + font-weight: 700; + color: var(--pk-primary); + padding: 0; + background: transparent; + border: none; + cursor: pointer; + text-decoration: none; + transition: all 0.2s ease; +} + +.viewBtn:hover { + gap: 0.75rem; + text-decoration: underline; +} + +.loadingCell, +.emptyCell { + text-align: center; + padding: 4rem !important; + color: #64748b; + font-style: italic; +} + +/* Receipt Modal Styles */ +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(8px); + display: flex; + align-items: center; + justify-content: center; + z-index: 2000; + padding: 2rem; +} + +.receiptModal { + background: #f8fafc; + width: 100%; + max-width: 450px; + border-radius: 24px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.modalHeader { + padding: 1.5rem; + background: white; + border-bottom: 1px solid #e2e8f0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.modalTitleRow { + display: flex; + align-items: center; + gap: 1rem; +} + +.modalTitleRow h3 { + margin: 0; + font-size: 1.1rem; + color: #0f172a; + font-weight: 800; +} + +.modalTitleRow span { + font-size: 0.8rem; + color: #64748b; + font-weight: 600; +} + +.closeBtn { + background: #f1f5f9; + border: none; + width: 36px; + height: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + color: #64748b; + cursor: pointer; + transition: all 0.2s ease; +} + +.closeBtn:hover { + background: #e2e8f0; + color: #0f172a; +} + +.receiptContent { + padding: 2rem; + background: white; + margin: 1.5rem; + border-radius: 16px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); + display: flex; + flex-direction: column; + gap: 1.25rem; + position: relative; +} + +.receiptHeader { + text-align: center; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.shopBrand { + font-size: 1.5rem; + font-weight: 900; + letter-spacing: 2px; + color: #0f172a; +} + +.shopAddress { + font-size: 0.75rem; + color: #64748b; + margin-bottom: 0.5rem; +} + +.receiptMeta { + display: flex; + justify-content: space-between; + font-size: 0.8rem; + font-weight: 700; + color: #1e293b; + background: #f8fafc; + padding: 0.5rem 1rem; + border-radius: 8px; +} + +.receiptDivider { + border-top: 2px dashed #e2e8f0; + margin: 0.5rem 0; +} + +.itemList { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.receiptItem { + display: flex; + justify-content: space-between; + align-items: baseline; + font-size: 0.9rem; +} + +.itemMain { + display: flex; + gap: 0.75rem; +} + +.itemQty { + font-weight: 800; + color: #64748b; + min-width: 24px; +} + +.itemName { + font-weight: 600; + color: #1e293b; +} + +.itemPrice { + font-weight: 700; + color: #0f172a; +} + +.totalsGroup { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.totalRow { + display: flex; + justify-content: space-between; + font-size: 0.85rem; + color: #64748b; + font-weight: 600; +} + +.grandTotal { + margin-top: 0.5rem; + padding-top: 1rem; + border-top: 1px solid #e2e8f0; + font-size: 1.25rem; + color: #0f172a; + font-weight: 900; +} + +.paymentInfo { + background: #f0fdf4; + padding: 0.75rem; + border-radius: 12px; + border: 1px solid #dcfce7; +} + +.payMethod { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + color: #15803d; + font-weight: 700; + font-size: 0.85rem; +} + +.barcodePlaceholder { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + margin-top: 1rem; +} + +.barcode { + width: 200px; + height: 40px; + background: repeating-linear-gradient(90deg, + #000, + #000 2px, + #fff 2px, + #fff 4px, + #000 4px, + #000 5px); + opacity: 0.3; +} + +.barcodePlaceholder span { + font-size: 0.75rem; + color: #94a3b8; + font-style: italic; +} + +.modalFooter { + padding: 1.5rem; + background: white; + border-top: 1px solid #e2e8f0; + display: flex; + gap: 0.75rem; +} + +.actionBtn { + flex: 1; + padding: 0.75rem; + border-radius: 12px; + border: 1px solid #e2e8f0; + background: white; + color: #1e293b; + font-size: 0.85rem; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + transition: all 0.2s ease; +} + +.actionBtn:hover { + background: #f8fafc; + border-color: #cbd5e1; +} + +.primaryAction { + background: var(--pk-primary); + border-color: var(--pk-primary); + color: white; +} + +.primaryAction:hover { + background: var(--pk-primary-hover); + border-color: var(--pk-primary-hover); +} \ No newline at end of file diff --git a/src/app/restaurant/orders/page.tsx b/src/app/restaurant/orders/page.tsx new file mode 100644 index 0000000..23566be --- /dev/null +++ b/src/app/restaurant/orders/page.tsx @@ -0,0 +1,275 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import AppLayout from '@/components/AppLayout/AppLayout'; +import { + Utensils, + LayoutDashboard, + Monitor, + Plus, + Search, + Filter, + ArrowRight, + Clock, + CheckCircle2, + Receipt, + X, + Printer, + Download, + Mail, + CreditCard +} from 'lucide-react'; +import styles from './orders.module.css'; +import Link from 'next/link'; +import { motion, AnimatePresence } from 'framer-motion'; + +const sidebarItems = [ + { icon: , label: 'Floor Plan' }, + { icon: , label: 'Orders', active: true }, + { icon: , label: 'KDS (Kitchen)' }, + { icon: , label: 'Menu Management' }, +]; + +export default function OrdersListPage() { + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [filterStatus, setFilterStatus] = useState('ALL'); + const [selectedOrder, setSelectedOrder] = useState(null); + + useEffect(() => { + const fetchOrders = async () => { + try { + const res = await fetch('http://localhost:5000/api/pos/orders'); + if (res.ok) { + const data = await res.json(); + setOrders(data); + } + } catch (err) { + console.error("Error fetching orders:", err); + } finally { + setLoading(false); + } + }; + + fetchOrders(); + }, []); + + const filteredOrders = orders.filter(order => { + const matchesSearch = + (order.orderId?.toLowerCase().includes(searchTerm.toLowerCase())) || + (order.tableId?.toString().includes(searchTerm)); + + const matchesStatus = filterStatus === 'ALL' || order.status === filterStatus; + + return matchesSearch && matchesStatus; + }); + + return ( + +
+
+
+
+ Total Orders + {orders.length} +
+
+ Pending Settlement + + {orders.filter(o => o.status !== 'PAID').length} + +
+
+ Revenue (Paid) + + $ {orders.filter(o => o.status === 'PAID').reduce((acc, o) => acc + (o.total || 0), 0).toFixed(2)} + +
+
+ +
+
+ + setSearchTerm(e.target.value)} + /> +
+
+ {['ALL', 'PENDING', 'ORDERED', 'PAID'].map(status => ( + + ))} +
+
+
+ +
+ + + + + + + + + + + + + + {loading ? ( + + + + ) : filteredOrders.length === 0 ? ( + + + + ) : filteredOrders.map(order => ( + + + + + + + + + + ))} + +
Order IDTableTimeItemsTotalStatusAction
Loading orders...
No orders found.
#{order.orderId || 'NEW'}Table {order.tableId} + + {new Date(order.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + {order.items?.length || 0} items$ {order.total?.toFixed(2) || '0.00'} + + {order.status === 'PAID' ? : } + {order.status} + + + {order.status === 'PAID' ? ( + + ) : ( + + Resume POS + + + )} +
+
+ + {/* Receipt Modal */} + + {selectedOrder && ( +
setSelectedOrder(null)}> + e.stopPropagation()} + > +
+
+ +
+

Order Receipt

+ #{selectedOrder.orderId || 'POS-2026-001'} +
+
+ +
+ +
+
+
DINE360
+
123 Culinary Ave, Food District
+
+ Table {selectedOrder.tableId} + {new Date(selectedOrder.createdAt).toLocaleString()} +
+
+ +
+ +
+ {selectedOrder.items?.map((item: any, i: number) => ( +
+
+ {item.qty}x + {item.product?.name || 'Product'} +
+ $ {(item.product?.price * item.qty).toFixed(2)} +
+ ))} +
+ +
+ +
+
+ Subtotal + $ {selectedOrder.subtotal?.toFixed(2)} +
+
+ Tax (5%) + $ {selectedOrder.tax?.toFixed(2)} +
+
+ Service Charge (5%) + $ {selectedOrder.serviceCharge?.toFixed(2)} +
+
+ TOTAL + $ {selectedOrder.total?.toFixed(2)} +
+
+ +
+
+ + Paid via {selectedOrder.paymentMethod || 'CASH'} +
+
+ +
+
+ Thank you for dining with us! +
+
+ +
+ + + +
+
+
+ )} +
+
+
+ ); +} diff --git a/src/app/restaurant/page.tsx b/src/app/restaurant/page.tsx index ab4e5f5..c0e7549 100644 --- a/src/app/restaurant/page.tsx +++ b/src/app/restaurant/page.tsx @@ -38,20 +38,61 @@ const initialTables = [ ]; export default function RestaurantPage() { - const [activeFloor, setActiveFloor] = useState('Main Floor'); + const [floors, setFloors] = useState([]); + const [activeFloorId, setActiveFloorId] = useState(null); const [isEditing, setIsEditing] = useState(false); - const [tables, setTables] = useState(initialTables); - const [selectedTable, setSelectedTable] = useState(null); - const [user, setUser] = useState<{ name: string; role: string } | null>(null); + const [tables, setTables] = useState([]); + const [selectedTableId, setSelectedTableId] = useState(null); + const [user, setUser] = useState<{ id: string; name: string; role: string } | null>(null); + const [activeOrderTables, setActiveOrderTables] = useState([]); + const [loading, setLoading] = useState(true); + + const [currentView, setCurrentView] = useState('Plan'); + const [isFloorModalOpen, setIsFloorModalOpen] = useState(false); + const [newFloorName, setNewFloorName] = useState(''); + + // Fetch All Data + const fetchData = async () => { + try { + const [userRes, floorsRes, tablesRes, ordersRes] = await Promise.all([ + fetch('/api/auth/me'), + fetch('http://localhost:5000/api/pos/floors'), + fetch('http://localhost:5000/api/pos/tables'), + fetch('http://localhost:5000/api/pos/orders?active=true') + ]); + + if (userRes.ok) { + const userData = await userRes.json(); + setUser(userData.user); + } + + if (floorsRes.ok) { + const floorsData = await floorsRes.json(); + setFloors(floorsData); + if (floorsData.length > 0 && !activeFloorId) setActiveFloorId(floorsData[0].id); + } + + if (tablesRes.ok) { + const tablesData = await tablesRes.json(); + setTables(tablesData); + } + + if (ordersRes.ok) { + const ordersData = await ordersRes.json(); + const tableIds = Array.from(new Set(ordersData.map((o: any) => o.tableId))).filter(id => id !== undefined) as number[]; + setActiveOrderTables(tableIds); + } + } catch (err) { + console.error("Error fetching restaurant data:", err); + } finally { + setLoading(false); + } + }; useEffect(() => { - fetch('/api/auth/me') - .then(res => res.json()) - .then(data => { - if (data.user) { - setUser(data.user); - } - }); + fetchData(); + const interval = setInterval(fetchData, 10000); + return () => clearInterval(interval); }, []); const canEdit = user?.role === 'admin' || user?.role === 'manager'; @@ -63,173 +104,351 @@ export default function RestaurantPage() { }; const addTable = () => { + if (!activeFloorId) return; const newId = tables.length > 0 ? Math.max(...tables.map(t => t.id)) + 1 : 1; - setTables([...tables, { + const newTable = { id: newId, + floorId: activeFloorId, name: `T${newId}`, seats: 4, status: 'available', x: 50, y: 50, shape: 'rect' - }]); - setSelectedTable(newId); + }; + setTables([...tables, newTable]); + setSelectedTableId(newId); + if (!isEditing) setIsEditing(true); + }; + + const addFloor = async () => { + if (!newFloorName) return; + try { + const res = await fetch('http://localhost:5000/api/pos/floors', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: newFloorName }) + }); + if (res.ok) { + const data = await res.json(); + setFloors([...floors, data]); + setActiveFloorId(data.id); + setIsFloorModalOpen(false); + setNewFloorName(''); + } + } catch (err) { + console.error(err); + } }; const deleteTable = (id: number) => { setTables(tables.filter(t => t.id !== id)); - setSelectedTable(null); + setSelectedTableId(null); }; - const updateTable = (id: number, updates: any) => { + const updateTableLocal = (id: number, updates: any) => { setTables(tables.map(t => t.id === id ? { ...t, ...updates } : t)); }; - const currentTable = tables.find(t => t.id === selectedTable); + const saveFloorPlan = async () => { + setIsEditing(false); + try { + await Promise.all(tables.map(table => + fetch(`http://localhost:5000/api/pos/tables/${table.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(table) + }) + )); + alert("Floor plan saved successfully!"); + } catch (err) { + console.error("Error saving plan:", err); + alert("Failed to save floor plan."); + } + }; + + const currentTable = tables.find(t => t.id === selectedTableId); + const filteredTables = tables.filter(t => t.floorId === activeFloorId); + + if (loading) return
Loading Floor Plan...
; return ( - + currentView === 'Plan' ? addTable() : setIsFloorModalOpen(true)} + >
- {/* Floor Navigation */} -
- {['Main Floor', 'Terrace', 'Bar'].map(floor => ( - - ))} - - - {canEdit && ( -
- - {isEditing && ( - + ))} + + {canEdit && ( + <> + + +
+ + {isEditing && ( + + )} +
+ )}
- )} -
- {/* Main Floor Area */} -
- {!isEditing && ( -
-
Available
-
Occupied
-
Reserved
+ {/* Main Floor Area */} +
+ {!isEditing && ( +
+
Available
+
Occupied
+
Reserved
+
+ )} + +
+ {filteredTables.map(table => { + const isOccupied = activeOrderTables.includes(table.id); + const displayStatus = isOccupied ? 'occupied' : table.status; + + return ( + handleDrag(table.id, info)} + className={`${styles.table} ${styles[displayStatus]} ${styles[table.shape]} ${selectedTableId === table.id && isEditing ? styles.selectedTable : ''}`} + style={{ left: table.x, top: table.y, position: 'absolute' }} + onClick={() => isEditing && setSelectedTableId(table.id)} + > + {!isEditing ? ( + +
+ {table.name} + {table.seats} seats + {isOccupied && ACTIVE} +
+ + ) : ( +
+
+ {table.name} +
+ )} +
+ ); + })} +
- )} -
- {tables.map(table => ( - handleDrag(table.id, info)} - className={`${styles.table} ${styles[table.status]} ${styles[table.shape]} ${selectedTable === table.id && isEditing ? styles.selectedTable : ''}`} - style={{ left: table.x, top: table.y, position: 'absolute' }} - onClick={() => isEditing && setSelectedTable(table.id)} - > - {!isEditing ? ( - -
- {table.name} - {table.seats} seats - {table.orderTotal && {table.orderTotal}} -
- - ) : ( -
-
- {table.name} + {/* Designer Sidebar / KDS Sidebar */} +
+ {isEditing && selectedTableId ? ( +
+
+

Table Properties

+
- )} - +
+ + updateTableLocal(selectedTableId, { name: e.target.value })} + /> +
+
+ + updateTableLocal(selectedTableId, { seats: parseInt(e.target.value) || 0 })} + /> +
+
+ +
+ + +
+
+
+ ) : ( +
+
+

Kitchen Status

+ {activeOrderTables.length} Active +
+
+ {activeOrderTables.length > 0 ? ( + activeOrderTables.map(tId => ( +
+
+ Table {tId} + Ongoing Service +
+ +
+ )) + ) : ( +
+ No active kitchen orders +
+ )} +
+ View Kitchen Board +
+ )} +
+ + )} + + {currentView === 'List' && ( +
+ + + + + + + + + + + {tables.map(table => ( + + + + + + + ))} + +
Table NameFloorSeatsStatus
{table.name}{floors.find(f => f.id === table.floorId)?.name || 'Unknown'}{table.seats} + + {activeOrderTables.includes(table.id) ? 'Occupied' : 'Available'} + +
+
+ )} + + {currentView === 'Kanban' && ( +
+ {['Available', 'Occupied'].map(status => ( +
+

{status}

+ {tables.filter(t => (status === 'Occupied' ? activeOrderTables.includes(t.id) : !activeOrderTables.includes(t.id))).map(table => ( +
+
+

{table.name}

+ +
+

+ {floors.find(f => f.id === table.floorId)?.name} • {table.seats} Seats +

+
+ ))} +
))}
-
+ )} - {/* Designer Sidebar / KDS Sidebar */} -
- {isEditing && selectedTable ? ( -
-
-

Table Properties

- + {currentView === 'Graph' && ( +
+
+

Table Occupancy

+
+ {floors.map(floor => { + const floorTables = tables.filter(t => t.floorId === floor.id); + const occupied = floorTables.filter(t => activeOrderTables.includes(t.id)).length; + const percentage = floorTables.length > 0 ? (occupied / floorTables.length) * 100 : 0; + return ( +
+
+ {floor.name} + {Math.round(percentage)}% +
+ ); + })} +
+
+
+

Average Usage

+

+ {Math.round((activeOrderTables.length / (tables.length || 1)) * 100)}% +

+

Current Occupancy Across Restaurant

+
+
+ )} + + {/* Add Floor Modal */} + {isFloorModalOpen && ( +
+
+
+

Add New Floor

- + updateTable(selectedTable, { name: e.target.value })} + placeholder="e.g. Roof Top, Garden" + value={newFloorName} + onChange={(e) => setNewFloorName(e.target.value)} + autoFocus />
-
- - updateTable(selectedTable, { seats: parseInt(e.target.value) })} - /> -
-
- -
- - -
+
+ +
- ) : ( -
-
-

Kitchen Status

- 3 Ready -
-
-
-
- Burger x2 - Table T2 -
- -
-
-
- Pizza x1 - Table T5 -
- -
-
- View Full KDS -
- )} -
+
+ )}
); diff --git a/src/app/restaurant/restaurant.module.css b/src/app/restaurant/restaurant.module.css index c3b3796..07a9a24 100644 --- a/src/app/restaurant/restaurant.module.css +++ b/src/app/restaurant/restaurant.module.css @@ -416,9 +416,101 @@ font-size: 0.85rem; color: var(--pk-primary); text-decoration: none; - font-weight: 700; + font-size: 0.85rem; + text-transform: uppercase; +} + +.listTable td { + padding: 1rem; + border-bottom: 1px solid #f1f5f9; + font-size: 0.95rem; + color: #1e293b; +} + +/* Kanban View Styles */ +.kanbanView { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.5rem; + width: 100%; +} + +.kanbanCard { + background: white; + padding: 1.5rem; + border-radius: 20px; + border: 1px solid #e2e8f0; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + gap: 1rem; +} + +.kanbanHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.kanbanHeader h4 { + margin: 0; + font-size: 1.25rem; + color: #0f172a; +} + +/* Graph View Styles */ +.graphView { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + width: 100%; +} + +.chartCard { + background: white; + padding: 2rem; + border-radius: 24px; + border: 1px solid #e2e8f0; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); +} + +/* Modal Styles */ +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; - gap: 0.5rem; + z-index: 2000; +} + +.modalContent { + background: white; + width: 400px; + padding: 2rem; + border-radius: 24px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); +} + +.modalHeader { + margin-bottom: 1.5rem; +} + +.modalHeader h2 { + font-size: 1.5rem; + font-weight: 800; + color: #0f172a; + margin: 0; +} + +.modalFooter { + display: flex; + justify-content: flex-end; + gap: 1rem; + margin-top: 2rem; } \ No newline at end of file diff --git a/src/components/AppLayout/AppLayout.module.css b/src/components/AppLayout/AppLayout.module.css index 6568cba..8d9a08d 100644 --- a/src/components/AppLayout/AppLayout.module.css +++ b/src/components/AppLayout/AppLayout.module.css @@ -230,9 +230,9 @@ color: #64748b; } -.viewBtn:first-child { - background: white; - color: var(--text-main); +.activeView { + background: white !important; + color: var(--text-main) !important; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } diff --git a/src/components/AppLayout/AppLayout.tsx b/src/components/AppLayout/AppLayout.tsx index 23d3236..386f86c 100644 --- a/src/components/AppLayout/AppLayout.tsx +++ b/src/components/AppLayout/AppLayout.tsx @@ -19,10 +19,13 @@ interface AppLayoutProps { children: React.ReactNode; title: string; sidebarItems: { icon: React.ReactNode; label: string; active?: boolean }[]; + currentView?: string; + onViewChange?: (view: string) => void; + onNewClick?: () => void; } -export default function AppLayout({ children, title, sidebarItems }: AppLayoutProps) { - const [user, setUser] = useState<{ name: string; role: string } | null>(null); +export default function AppLayout({ children, title, sidebarItems, currentView = 'List', onViewChange, onNewClick }: AppLayoutProps) { + const [user, setUser] = useState<{ id: string; name: string; role: string } | null>(null); const router = useRouter(); useEffect(() => { @@ -64,12 +67,15 @@ export default function AppLayout({ children, title, sidebarItems }: AppLayoutPr {filteredSidebarItems.map((item: any, idx: number) => { let href = '#'; if (item.label === 'Floor Plan') href = '/restaurant'; + else if (item.label === 'Orders' && title === 'Restaurant') href = '/restaurant/orders'; + else if (item.label === 'Orders') href = '/sales'; + else if (item.label === 'KDS (Kitchen)') href = '/kitchen'; else if (item.label === 'Menu Management') href = '/restaurant/menu'; else if (item.label === 'Waitstaff Assignment') href = '/restaurant/waitstaff'; else if (item.label === 'Financial Reports') href = '/financials'; else if (item.label === 'Journal Entries') href = '/accounting'; - else if (item.label === 'Payslips') href = '/payroll'; else if (item.label === 'Accounting Dashboard') href = '/accounting'; + else if (item.label === 'Payslips') href = '/payroll'; else if (item.label === 'Employee Dashboard') href = '/payroll'; return ( @@ -93,14 +99,14 @@ export default function AppLayout({ children, title, sidebarItems }: AppLayoutPr
{title}
- Orders / Sales Orders + Dine360 / {title}
- +
@@ -113,14 +119,21 @@ export default function AppLayout({ children, title, sidebarItems }: AppLayoutPr {/* Action Bar */}
- + +
- - - + {['Plan', 'List', 'Kanban', 'Graph'].map(view => ( + + ))}