restaurant functionality updated

This commit is contained in:
Alaguraj0361 2026-01-22 22:19:06 +05:30
parent b7e599d0b2
commit 64ef8a3472
9 changed files with 1275 additions and 177 deletions

View File

@ -7,7 +7,7 @@ const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
export async function GET() { export async function GET() {
try { try {
const cookieStore = await cookies(); const cookieStore = await cookies();
const token = cookieStore.get('token')?.value; const token = cookieStore.get('auth_token')?.value;
if (!token) { if (!token) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });

View File

@ -713,13 +713,13 @@ export default function POSPage() {
if (step === 'DASHBOARD') { if (step === 'DASHBOARD') {
return ( return (
<div className={styles.posContainer}> <div className={styles.posContainer}>
<header className={styles.header} style={{ background: '#0f172a', border: 'none' }}> <header className={styles.header} style={{ background: '#ffffff', borderBottom: '1px solid #e2e8f0' }}>
<div className={styles.navLeft}> <div className={styles.navLeft}>
<Link href="/" className={styles.backBtn} style={{ color: 'white' }}> <Link href="/" className={styles.backBtn} style={{ color: '#0f172a' }}>
<ChevronLeft size={20} /> <ChevronLeft size={20} />
<span>Dashboard</span> <span>Dashboard</span>
</Link> </Link>
<span className={styles.shopName} style={{ color: '#2dd4bf' }}>Point of Sale</span> <span className={styles.shopName} style={{ color: '#0d9488', fontWeight: 700 }}>Point of Sale</span>
</div> </div>
</header> </header>
@ -735,7 +735,7 @@ export default function POSPage() {
<div className={styles.sessionCardHeader}> <div className={styles.sessionCardHeader}>
<span className={styles.sessionName}>{session.name}</span> <span className={styles.sessionName}>{session.name}</span>
<div className={styles.sessionIcon}> <div className={styles.sessionIcon}>
{session.isRestaurant ? <LayoutGrid size={32} color="#2dd4bf" /> : <ShoppingCart size={32} color="#2dd4bf" />} {session.isRestaurant ? <LayoutGrid size={32} color="#0d9488" /> : <ShoppingCart size={32} color="#0d9488" />}
</div> </div>
</div> </div>
<div className={styles.sessionBadges}> <div className={styles.sessionBadges}>
@ -891,11 +891,11 @@ export default function POSPage() {
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div className={styles.propGroup}> <div className={styles.propGroup}>
<label>Identifier</label> <label>Identifier</label>
<input type="text" value={selectedTable.name} onChange={(e) => updateTable(selectedTable.id, { name: e.target.value })} /> <input type="text" value={selectedTable.name || ''} onChange={(e) => updateTable(selectedTable.id, { name: e.target.value })} />
</div> </div>
<div className={styles.propGroup}> <div className={styles.propGroup}>
<label>Seats</label> <label>Seats</label>
<input type="number" value={selectedTable.seats} onChange={(e) => updateTable(selectedTable.id, { seats: parseInt(e.target.value) || 0 })} /> <input type="number" value={selectedTable.seats || 0} onChange={(e) => updateTable(selectedTable.id, { seats: parseInt(e.target.value) || 0 })} />
</div> </div>
<div className={styles.propGroup}> <div className={styles.propGroup}>
<label>Shape</label> <label>Shape</label>

View File

@ -119,12 +119,12 @@
border: 2px solid transparent; border: 2px solid transparent;
} }
/* Dashboard Upgrade */ /* Dashboard Upgrade - Light Version */
.dashboardContainer { .dashboardContainer {
flex: 1; flex: 1;
padding: 3rem; padding: 3rem;
overflow-y: auto; overflow-y: auto;
background: radial-gradient(circle at top right, #1e293b 0%, #0b0f19 100%); background: #f8fafc;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 3rem; gap: 3rem;
@ -138,7 +138,7 @@
.dashboardTitle { .dashboardTitle {
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 800; font-weight: 800;
background: linear-gradient(to right, #f8fafc, #2dd4bf); background: linear-gradient(to right, #0f172a, #0d9488);
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
@ -146,7 +146,7 @@
} }
.dashboardSubtitle { .dashboardSubtitle {
color: #94a3b8; color: #64748b;
font-size: 1.1rem; font-size: 1.1rem;
} }
@ -158,31 +158,50 @@
} }
.sessionCard { .sessionCard {
background: rgba(30, 41, 59, 0.7); background: #ffffff;
backdrop-filter: blur(12px);
border-radius: 28px; border-radius: 28px;
padding: 2.5rem; padding: 2.5rem;
position: relative; 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); transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 280px; min-height: 280px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
} }
.sessionCard:hover { .sessionCard:hover {
transform: translateY(-12px) scale(1.02); transform: translateY(-12px) scale(1.02);
border-color: rgba(45, 212, 191, 0.4); border-color: #2dd4bf;
box-shadow: 0 40px 60px -20px rgba(0, 0, 0, 0.6); box-shadow: 0 40px 60px -20px rgba(0, 0, 0, 0.1);
} }
.sessionName { .sessionName {
font-size: 1.8rem; font-size: 1.8rem;
font-weight: 800; font-weight: 800;
color: #f8fafc; color: #0f172a;
letter-spacing: -0.5px; 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 { .sessionBadges {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -199,19 +218,19 @@
} }
.badgeInfo { .badgeInfo {
background: rgba(8, 145, 178, 0.2); background: #e0f2fe;
color: #22d3ee; color: #0369a1;
} }
.badgeWarn { .badgeWarn {
background: rgba(185, 28, 28, 0.2); background: #fee2e2;
color: #f87171; color: #b91c1c;
} }
.continueBtn { .continueBtn {
margin-top: auto; margin-top: auto;
background: linear-gradient(135deg, #2dd4bf, #14b8a6); background: linear-gradient(135deg, #0d9488, #0f766e);
color: #0f172a; color: #ffffff;
border: none; border: none;
padding: 1.2rem 2rem; padding: 1.2rem 2rem;
border-radius: 18px; border-radius: 18px;
@ -219,6 +238,14 @@
font-size: 1.1rem; font-size: 1.1rem;
cursor: pointer; cursor: pointer;
text-align: center; 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 { .userInitial {

View File

@ -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);
}

View File

@ -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: <LayoutDashboard size={20} />, label: 'Floor Plan' },
{ icon: <Utensils size={20} />, label: 'Orders', active: true },
{ icon: <Monitor size={20} />, label: 'KDS (Kitchen)' },
{ icon: <Plus size={20} />, label: 'Menu Management' },
];
export default function OrdersListPage() {
const [orders, setOrders] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [filterStatus, setFilterStatus] = useState('ALL');
const [selectedOrder, setSelectedOrder] = useState<any | null>(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 (
<AppLayout title="Orders" sidebarItems={sidebarItems}>
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.statsRow}>
<div className={styles.statCard}>
<span className={styles.statLabel}>Total Orders</span>
<span className={styles.statValue}>{orders.length}</span>
</div>
<div className={styles.statCard}>
<span className={styles.statLabel}>Pending Settlement</span>
<span className={styles.statValue} style={{ color: '#f59e0b' }}>
{orders.filter(o => o.status !== 'PAID').length}
</span>
</div>
<div className={styles.statCard}>
<span className={styles.statLabel}>Revenue (Paid)</span>
<span className={styles.statValue} style={{ color: '#10b981' }}>
$ {orders.filter(o => o.status === 'PAID').reduce((acc, o) => acc + (o.total || 0), 0).toFixed(2)}
</span>
</div>
</div>
<div className={styles.filterBar}>
<div className={styles.search}>
<Search size={18} />
<input
type="text"
placeholder="Search by Order ID or Table..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className={styles.statusFilters}>
{['ALL', 'PENDING', 'ORDERED', 'PAID'].map(status => (
<button
key={status}
className={`${styles.filterBtn} ${filterStatus === status ? styles.activeFilter : ''}`}
onClick={() => setFilterStatus(status)}
>
{status}
</button>
))}
</div>
</div>
</div>
<div className={styles.tableWrapper}>
<table className={styles.ordersTable}>
<thead>
<tr>
<th>Order ID</th>
<th>Table</th>
<th>Time</th>
<th>Items</th>
<th>Total</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={7} className={styles.loadingCell}>Loading orders...</td>
</tr>
) : filteredOrders.length === 0 ? (
<tr>
<td colSpan={7} className={styles.emptyCell}>No orders found.</td>
</tr>
) : filteredOrders.map(order => (
<tr key={order._id}>
<td className={styles.orderId}>#{order.orderId || 'NEW'}</td>
<td><span className={styles.tableBadge}>Table {order.tableId}</span></td>
<td className={styles.timeCell}>
<Clock size={14} />
{new Date(order.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</td>
<td>{order.items?.length || 0} items</td>
<td className={styles.totalCell}>$ {order.total?.toFixed(2) || '0.00'}</td>
<td>
<span className={`${styles.statusBadge} ${styles[order.status?.toLowerCase()]}`}>
{order.status === 'PAID' ? <CheckCircle2 size={12} /> : <Clock size={12} />}
{order.status}
</span>
</td>
<td>
{order.status === 'PAID' ? (
<button
className={styles.viewBtn}
onClick={() => setSelectedOrder(order)}
>
View Receipt
<Receipt size={14} />
</button>
) : (
<Link href={`/pos?table=${order.tableId}`} className={styles.viewBtn}>
Resume POS
<ArrowRight size={14} />
</Link>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Receipt Modal */}
<AnimatePresence>
{selectedOrder && (
<div className={styles.modalOverlay} onClick={() => setSelectedOrder(null)}>
<motion.div
className={styles.receiptModal}
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
onClick={(e) => e.stopPropagation()}
>
<div className={styles.modalHeader}>
<div className={styles.modalTitleRow}>
<Receipt size={24} color="var(--pk-primary)" />
<div>
<h3>Order Receipt</h3>
<span>#{selectedOrder.orderId || 'POS-2026-001'}</span>
</div>
</div>
<button className={styles.closeBtn} onClick={() => setSelectedOrder(null)}>
<X size={20} />
</button>
</div>
<div className={styles.receiptContent}>
<div className={styles.receiptHeader}>
<div className={styles.shopBrand}>DINE360</div>
<div className={styles.shopAddress}>123 Culinary Ave, Food District</div>
<div className={styles.receiptMeta}>
<span>Table {selectedOrder.tableId}</span>
<span>{new Date(selectedOrder.createdAt).toLocaleString()}</span>
</div>
</div>
<div className={styles.receiptDivider}></div>
<div className={styles.itemList}>
{selectedOrder.items?.map((item: any, i: number) => (
<div key={i} className={styles.receiptItem}>
<div className={styles.itemMain}>
<span className={styles.itemQty}>{item.qty}x</span>
<span className={styles.itemName}>{item.product?.name || 'Product'}</span>
</div>
<span className={styles.itemPrice}>$ {(item.product?.price * item.qty).toFixed(2)}</span>
</div>
))}
</div>
<div className={styles.receiptDivider}></div>
<div className={styles.totalsGroup}>
<div className={styles.totalRow}>
<span>Subtotal</span>
<span>$ {selectedOrder.subtotal?.toFixed(2)}</span>
</div>
<div className={styles.totalRow}>
<span>Tax (5%)</span>
<span>$ {selectedOrder.tax?.toFixed(2)}</span>
</div>
<div className={styles.totalRow}>
<span>Service Charge (5%)</span>
<span>$ {selectedOrder.serviceCharge?.toFixed(2)}</span>
</div>
<div className={`${styles.totalRow} ${styles.grandTotal}`}>
<span>TOTAL</span>
<span>$ {selectedOrder.total?.toFixed(2)}</span>
</div>
</div>
<div className={styles.paymentInfo}>
<div className={styles.payMethod}>
<CreditCard size={16} />
<span>Paid via {selectedOrder.paymentMethod || 'CASH'}</span>
</div>
</div>
<div className={styles.barcodePlaceholder}>
<div className={styles.barcode}></div>
<span>Thank you for dining with us!</span>
</div>
</div>
<div className={styles.modalFooter}>
<button className={styles.actionBtn} onClick={() => window.print()}>
<Printer size={18} /> Print
</button>
<button className={styles.actionBtn}>
<Download size={18} /> PDF
</button>
<button className={`${styles.actionBtn} ${styles.primaryAction}`}>
<Mail size={18} /> Email Receipt
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
</AppLayout>
);
}

View File

@ -38,20 +38,61 @@ const initialTables = [
]; ];
export default function RestaurantPage() { export default function RestaurantPage() {
const [activeFloor, setActiveFloor] = useState('Main Floor'); const [floors, setFloors] = useState<any[]>([]);
const [activeFloorId, setActiveFloorId] = useState<number | null>(null);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [tables, setTables] = useState(initialTables); const [tables, setTables] = useState<any[]>([]);
const [selectedTable, setSelectedTable] = useState<number | null>(null); const [selectedTableId, setSelectedTableId] = useState<number | null>(null);
const [user, setUser] = useState<{ name: string; role: string } | null>(null); const [user, setUser] = useState<{ id: string; name: string; role: string } | null>(null);
const [activeOrderTables, setActiveOrderTables] = useState<number[]>([]);
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(() => { useEffect(() => {
fetch('/api/auth/me') fetchData();
.then(res => res.json()) const interval = setInterval(fetchData, 10000);
.then(data => { return () => clearInterval(interval);
if (data.user) {
setUser(data.user);
}
});
}, []); }, []);
const canEdit = user?.role === 'admin' || user?.role === 'manager'; const canEdit = user?.role === 'admin' || user?.role === 'manager';
@ -63,173 +104,351 @@ export default function RestaurantPage() {
}; };
const addTable = () => { const addTable = () => {
if (!activeFloorId) return;
const newId = tables.length > 0 ? Math.max(...tables.map(t => t.id)) + 1 : 1; const newId = tables.length > 0 ? Math.max(...tables.map(t => t.id)) + 1 : 1;
setTables([...tables, { const newTable = {
id: newId, id: newId,
floorId: activeFloorId,
name: `T${newId}`, name: `T${newId}`,
seats: 4, seats: 4,
status: 'available', status: 'available',
x: 50, x: 50,
y: 50, y: 50,
shape: 'rect' 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) => { const deleteTable = (id: number) => {
setTables(tables.filter(t => t.id !== id)); 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)); 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 <div style={{ padding: '2rem', color: 'white' }}>Loading Floor Plan...</div>;
return ( return (
<AppLayout title="Restaurant" sidebarItems={sidebarItems}> <AppLayout
title="Restaurant"
sidebarItems={sidebarItems}
currentView={currentView}
onViewChange={setCurrentView}
onNewClick={() => currentView === 'Plan' ? addTable() : setIsFloorModalOpen(true)}
>
<div className={styles.restaurantContainer}> <div className={styles.restaurantContainer}>
{/* Floor Navigation */} {currentView === 'Plan' && (
<div className={styles.floorNav}> <>
{['Main Floor', 'Terrace', 'Bar'].map(floor => ( {/* Floor Navigation */}
<button <div className={styles.floorNav}>
key={floor} {floors.map(floor => (
className={`${styles.floorBtn} ${activeFloor === floor ? styles.activeFloor : ''}`} <button
onClick={() => setActiveFloor(floor)} key={floor.id}
> className={`${styles.floorBtn} ${activeFloorId === floor.id ? styles.activeFloor : ''}`}
{floor} onClick={() => setActiveFloorId(floor.id)}
</button> >
))} {floor.name}
<button className={styles.addFloorBtn}><Plus size={16} /></button>
{canEdit && (
<div className={styles.editToggleSection}>
<button
className={`${styles.editToggle} ${isEditing ? styles.activeEdit : ''}`}
onClick={() => setIsEditing(!isEditing)}
>
{isEditing ? <><Save size={16} /> Save Plan</> : <><Settings size={16} /> Edit Floor</>}
</button>
{isEditing && (
<button className={styles.addTableBtn} onClick={addTable}>
<Plus size={16} /> Add Table
</button> </button>
))}
{canEdit && (
<>
<button className={styles.addFloorBtn} onClick={() => setIsFloorModalOpen(true)}>
<Plus size={16} /> New Floor
</button>
<div className={styles.editToggleSection}>
<button
className={`${styles.editToggle} ${isEditing ? styles.activeEdit : ''}`}
onClick={() => isEditing ? saveFloorPlan() : setIsEditing(true)}
>
{isEditing ? <><Save size={16} /> Save Plan</> : <><Settings size={16} /> Edit Floor</>}
</button>
{isEditing && (
<button className={styles.addTableBtn} onClick={addTable}>
<Plus size={16} /> Add Table
</button>
)}
</div>
</>
)} )}
</div> </div>
)}
</div>
{/* Main Floor Area */} {/* Main Floor Area */}
<div className={styles.floorPlan}> <div className={styles.floorPlan}>
{!isEditing && ( {!isEditing && (
<div className={styles.legend}> <div className={styles.legend}>
<div className={styles.legendItem}><span className={styles.statusDot} style={{ backgroundColor: '#10b981' }}></span> Available</div> <div className={styles.legendItem}><span className={styles.statusDot} style={{ backgroundColor: '#10b981' }}></span> Available</div>
<div className={styles.legendItem}><span className={styles.statusDot} style={{ backgroundColor: '#ef4444' }}></span> Occupied</div> <div className={styles.legendItem}><span className={styles.statusDot} style={{ backgroundColor: '#ef4444' }}></span> Occupied</div>
<div className={styles.legendItem}><span className={styles.statusDot} style={{ backgroundColor: '#f59e0b' }}></span> Reserved</div> <div className={styles.legendItem}><span className={styles.statusDot} style={{ backgroundColor: '#f59e0b' }}></span> Reserved</div>
</div>
)}
<div className={styles.canvas}>
{filteredTables.map(table => {
const isOccupied = activeOrderTables.includes(table.id);
const displayStatus = isOccupied ? 'occupied' : table.status;
return (
<motion.div
key={table.id}
drag={isEditing}
dragMomentum={false}
onDrag={(e, info) => 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 ? (
<Link href={`/pos?table=${table.id}`} className={styles.tableInnerLink}>
<div className={styles.tableInner}>
<span className={styles.tableName}>{table.name}</span>
<span className={styles.seats}>{table.seats} seats</span>
{isOccupied && <span className={styles.orderTotal} style={{ color: '#ef4444', fontWeight: 800 }}>ACTIVE</span>}
</div>
</Link>
) : (
<div className={styles.tableInner}>
<div className={styles.dragHandle}><Move size={14} /></div>
<span className={styles.tableName}>{table.name}</span>
</div>
)}
</motion.div>
);
})}
</div>
</div> </div>
)}
<div className={styles.canvas}> {/* Designer Sidebar / KDS Sidebar */}
{tables.map(table => ( <div className={styles.rightSidebar}>
<motion.div {isEditing && selectedTableId ? (
key={table.id} <div className={styles.propertyEditor}>
drag={isEditing} <div className={styles.sidebarHeader}>
dragMomentum={false} <h3>Table Properties</h3>
onDrag={(e, info) => handleDrag(table.id, info)} <button className={styles.deleteBtn} onClick={() => deleteTable(selectedTableId)}>
className={`${styles.table} ${styles[table.status]} ${styles[table.shape]} ${selectedTable === table.id && isEditing ? styles.selectedTable : ''}`} <Trash2 size={16} />
style={{ left: table.x, top: table.y, position: 'absolute' }} </button>
onClick={() => isEditing && setSelectedTable(table.id)}
>
{!isEditing ? (
<Link href={`/pos?table=${table.id}`} className={styles.tableInnerLink}>
<div className={styles.tableInner}>
<span className={styles.tableName}>{table.name}</span>
<span className={styles.seats}>{table.seats} seats</span>
{table.orderTotal && <span className={styles.orderTotal}>{table.orderTotal}</span>}
</div>
</Link>
) : (
<div className={styles.tableInner}>
<div className={styles.dragHandle}><Move size={14} /></div>
<span className={styles.tableName}>{table.name}</span>
</div> </div>
)} <div className={styles.propGroup}>
</motion.div> <label>Identifier</label>
<input
type="text"
value={currentTable?.name || ''}
onChange={(e) => updateTableLocal(selectedTableId, { name: e.target.value })}
/>
</div>
<div className={styles.propGroup}>
<label>Seats</label>
<input
type="number"
value={currentTable?.seats || 0}
onChange={(e) => updateTableLocal(selectedTableId, { seats: parseInt(e.target.value) || 0 })}
/>
</div>
<div className={styles.propGroup}>
<label>Shape</label>
<div className={styles.shapeToggle}>
<button
className={currentTable?.shape === 'rect' ? styles.activeShape : ''}
onClick={() => updateTableLocal(selectedTableId, { shape: 'rect' })}
>
Rectangle
</button>
<button
className={currentTable?.shape === 'circle' ? styles.activeShape : ''}
onClick={() => updateTableLocal(selectedTableId, { shape: 'circle' })}
>
Circle
</button>
</div>
</div>
</div>
) : (
<div className={styles.kdsPreview}>
<div className={styles.sidebarHeader}>
<h3>Kitchen Status</h3>
<span className={styles.kdsCount}>{activeOrderTables.length} Active</span>
</div>
<div className={styles.kdsList}>
{activeOrderTables.length > 0 ? (
activeOrderTables.map(tId => (
<div key={tId} className={styles.kdsItem}>
<div className={styles.kdsItemInfo}>
<strong>Table {tId}</strong>
<span>Ongoing Service</span>
</div>
<Clock size={18} color="#f59e0b" />
</div>
))
) : (
<div style={{ color: '#64748b', fontSize: '0.9rem', textAlign: 'center', marginTop: '2rem' }}>
No active kitchen orders
</div>
)}
</div>
<Link href="/kitchen" className={styles.viewFullKds}>View Kitchen Board <ArrowRight size={14} /></Link>
</div>
)}
</div>
</>
)}
{currentView === 'List' && (
<div className={styles.listView}>
<table className={styles.listTable}>
<thead>
<tr>
<th>Table Name</th>
<th>Floor</th>
<th>Seats</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{tables.map(table => (
<tr key={table.id}>
<td>{table.name}</td>
<td>{floors.find(f => f.id === table.floorId)?.name || 'Unknown'}</td>
<td>{table.seats}</td>
<td>
<span style={{
color: activeOrderTables.includes(table.id) ? '#ef4444' : '#10b981',
fontWeight: 700
}}>
{activeOrderTables.includes(table.id) ? 'Occupied' : 'Available'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{currentView === 'Kanban' && (
<div className={styles.kanbanView}>
{['Available', 'Occupied'].map(status => (
<div key={status} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<h3 style={{ textTransform: 'uppercase', fontSize: '0.8rem', color: '#64748b' }}>{status}</h3>
{tables.filter(t => (status === 'Occupied' ? activeOrderTables.includes(t.id) : !activeOrderTables.includes(t.id))).map(table => (
<div key={table.id} className={styles.kanbanCard}>
<div className={styles.kanbanHeader}>
<h4>{table.name}</h4>
<Users size={16} />
</div>
<p style={{ margin: 0, fontSize: '0.85rem', color: '#64748b' }}>
{floors.find(f => f.id === table.floorId)?.name} {table.seats} Seats
</p>
</div>
))}
</div>
))} ))}
</div> </div>
</div> )}
{/* Designer Sidebar / KDS Sidebar */} {currentView === 'Graph' && (
<div className={styles.rightSidebar}> <div className={styles.graphView}>
{isEditing && selectedTable ? ( <div className={styles.chartCard} style={{ flex: 2 }}>
<div className={styles.propertyEditor}> <h3>Table Occupancy</h3>
<div className={styles.sidebarHeader}> <div style={{ height: '300px', display: 'flex', alignItems: 'flex-end', gap: '2rem' }}>
<h3>Table Properties</h3> {floors.map(floor => {
<button className={styles.deleteBtn} onClick={() => deleteTable(selectedTable)}> const floorTables = tables.filter(t => t.floorId === floor.id);
<Trash2 size={16} /> const occupied = floorTables.filter(t => activeOrderTables.includes(t.id)).length;
</button> const percentage = floorTables.length > 0 ? (occupied / floorTables.length) * 100 : 0;
return (
<div key={floor.id} style={{ flex: 1, textAlign: 'center' }}>
<div style={{
height: `${Math.max(percentage, 5)}%`,
background: '#2dd4bf',
borderRadius: '8px 8px 0 0',
transition: 'height 1s ease',
boxShadow: '0 4px 12px rgba(45, 212, 191, 0.2)'
}}></div>
<span style={{ fontSize: '0.75rem', marginTop: '1rem', display: 'block', fontWeight: 600 }}>{floor.name}</span>
<span style={{ fontSize: '0.7rem', color: '#64748b' }}>{Math.round(percentage)}%</span>
</div>
);
})}
</div>
</div>
<div className={styles.chartCard} style={{ flex: 1, textAlign: 'center', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<h3 style={{ color: '#64748b' }}>Average Usage</h3>
<h1 style={{ fontSize: '5rem', margin: '1rem 0', color: '#0d9488', fontWeight: 900 }}>
{Math.round((activeOrderTables.length / (tables.length || 1)) * 100)}%
</h1>
<p style={{ color: '#64748b' }}>Current Occupancy Across Restaurant</p>
</div>
</div>
)}
{/* Add Floor Modal */}
{isFloorModalOpen && (
<div className={styles.modalOverlay}>
<div className={styles.modalContent}>
<div className={styles.modalHeader}>
<h2>Add New Floor</h2>
</div> </div>
<div className={styles.propGroup}> <div className={styles.propGroup}>
<label>Identifier</label> <label>Floor Name</label>
<input <input
type="text" type="text"
value={currentTable?.name} placeholder="e.g. Roof Top, Garden"
onChange={(e) => updateTable(selectedTable, { name: e.target.value })} value={newFloorName}
onChange={(e) => setNewFloorName(e.target.value)}
autoFocus
/> />
</div> </div>
<div className={styles.propGroup}> <div className={styles.modalFooter}>
<label>Seats</label> <button className={styles.deleteBtn} style={{ background: '#f1f5f9', color: '#64748b', borderColor: '#e2e8f0' }} onClick={() => setIsFloorModalOpen(false)}>Cancel</button>
<input <button className={styles.addTableBtn} onClick={addFloor}>Create Floor</button>
type="number"
value={currentTable?.seats}
onChange={(e) => updateTable(selectedTable, { seats: parseInt(e.target.value) })}
/>
</div>
<div className={styles.propGroup}>
<label>Shape</label>
<div className={styles.shapeToggle}>
<button
className={currentTable?.shape === 'rect' ? styles.activeShape : ''}
onClick={() => updateTable(selectedTable, { shape: 'rect' })}
>
Rectangle
</button>
<button
className={currentTable?.shape === 'circle' ? styles.activeShape : ''}
onClick={() => updateTable(selectedTable, { shape: 'circle' })}
>
Circle
</button>
</div>
</div> </div>
</div> </div>
) : ( </div>
<div className={styles.kdsPreview}> )}
<div className={styles.sidebarHeader}>
<h3>Kitchen Status</h3>
<span className={styles.kdsCount}>3 Ready</span>
</div>
<div className={styles.kdsList}>
<div className={styles.kdsItem}>
<div className={styles.kdsItemInfo}>
<strong>Burger x2</strong>
<span>Table T2</span>
</div>
<CheckCircle2 size={18} color="#10b981" />
</div>
<div className={styles.kdsItem}>
<div className={styles.kdsItemInfo}>
<strong>Pizza x1</strong>
<span>Table T5</span>
</div>
<Clock size={18} color="#f59e0b" />
</div>
</div>
<Link href="/restaurant/kds" className={styles.viewFullKds}>View Full KDS <ArrowRight size={14} /></Link>
</div>
)}
</div>
</div> </div>
</AppLayout> </AppLayout>
); );

View File

@ -416,9 +416,101 @@
font-size: 0.85rem; font-size: 0.85rem;
color: var(--pk-primary); color: var(--pk-primary);
text-decoration: none; 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; display: flex;
align-items: center; align-items: center;
justify-content: 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;
} }

View File

@ -230,9 +230,9 @@
color: #64748b; color: #64748b;
} }
.viewBtn:first-child { .activeView {
background: white; background: white !important;
color: var(--text-main); color: var(--text-main) !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
} }

View File

@ -19,10 +19,13 @@ interface AppLayoutProps {
children: React.ReactNode; children: React.ReactNode;
title: string; title: string;
sidebarItems: { icon: React.ReactNode; label: string; active?: boolean }[]; sidebarItems: { icon: React.ReactNode; label: string; active?: boolean }[];
currentView?: string;
onViewChange?: (view: string) => void;
onNewClick?: () => void;
} }
export default function AppLayout({ children, title, sidebarItems }: AppLayoutProps) { export default function AppLayout({ children, title, sidebarItems, currentView = 'List', onViewChange, onNewClick }: AppLayoutProps) {
const [user, setUser] = useState<{ name: string; role: string } | null>(null); const [user, setUser] = useState<{ id: string; name: string; role: string } | null>(null);
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
@ -64,12 +67,15 @@ export default function AppLayout({ children, title, sidebarItems }: AppLayoutPr
{filteredSidebarItems.map((item: any, idx: number) => { {filteredSidebarItems.map((item: any, idx: number) => {
let href = '#'; let href = '#';
if (item.label === 'Floor Plan') href = '/restaurant'; 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 === 'Menu Management') href = '/restaurant/menu';
else if (item.label === 'Waitstaff Assignment') href = '/restaurant/waitstaff'; else if (item.label === 'Waitstaff Assignment') href = '/restaurant/waitstaff';
else if (item.label === 'Financial Reports') href = '/financials'; else if (item.label === 'Financial Reports') href = '/financials';
else if (item.label === 'Journal Entries') href = '/accounting'; 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 === 'Accounting Dashboard') href = '/accounting';
else if (item.label === 'Payslips') href = '/payroll';
else if (item.label === 'Employee Dashboard') href = '/payroll'; else if (item.label === 'Employee Dashboard') href = '/payroll';
return ( return (
@ -93,14 +99,14 @@ export default function AppLayout({ children, title, sidebarItems }: AppLayoutPr
<div className={styles.navLeft}> <div className={styles.navLeft}>
<span className={styles.appTitle}>{title}</span> <span className={styles.appTitle}>{title}</span>
<div className={styles.breadcrumbs}> <div className={styles.breadcrumbs}>
<span>Orders</span> / <span className={styles.current}>Sales Orders</span> <span>Dine360</span> / <span className={styles.current}>{title}</span>
</div> </div>
</div> </div>
<div className={styles.navCenter}> <div className={styles.navCenter}>
<div className={styles.searchBar}> <div className={styles.searchBar}>
<Search size={16} className={styles.searchIcon} /> <Search size={16} className={styles.searchIcon} />
<input type="text" placeholder="Search orders..." className={styles.searchInput} /> <input type="text" placeholder={`Search ${title.toLowerCase()}...`} className={styles.searchInput} />
</div> </div>
</div> </div>
@ -113,14 +119,21 @@ export default function AppLayout({ children, title, sidebarItems }: AppLayoutPr
{/* Action Bar */} {/* Action Bar */}
<div className={styles.actionBar}> <div className={styles.actionBar}>
<div className={styles.actionLeft}> <div className={styles.actionLeft}>
<button className={styles.primaryBtn}><Plus size={18} /> New</button> <button className={styles.primaryBtn} onClick={onNewClick}><Plus size={18} /> New</button>
<button className={styles.secondaryBtn}>Import</button> <button className={styles.secondaryBtn}>Import</button>
<button className={styles.secondaryBtn} onClick={() => window.print()}>Print</button>
</div> </div>
<div className={styles.actionRight}> <div className={styles.actionRight}>
<div className={styles.viewToggle}> <div className={styles.viewToggle}>
<button className={styles.viewBtn}>List</button> {['Plan', 'List', 'Kanban', 'Graph'].map(view => (
<button className={styles.viewBtn}>Kanban</button> <button
<button className={styles.viewBtn}>Graph</button> key={view}
className={`${styles.viewBtn} ${currentView === view ? styles.activeView : ''}`}
onClick={() => onViewChange && onViewChange(view)}
>
{view}
</button>
))}
</div> </div>
</div> </div>
</div> </div>