restaurant functionality updated
This commit is contained in:
parent
b7e599d0b2
commit
64ef8a3472
@ -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 });
|
||||
|
||||
@ -713,13 +713,13 @@ export default function POSPage() {
|
||||
if (step === 'DASHBOARD') {
|
||||
return (
|
||||
<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}>
|
||||
<Link href="/" className={styles.backBtn} style={{ color: 'white' }}>
|
||||
<Link href="/" className={styles.backBtn} style={{ color: '#0f172a' }}>
|
||||
<ChevronLeft size={20} />
|
||||
<span>Dashboard</span>
|
||||
</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>
|
||||
</header>
|
||||
|
||||
@ -735,7 +735,7 @@ export default function POSPage() {
|
||||
<div className={styles.sessionCardHeader}>
|
||||
<span className={styles.sessionName}>{session.name}</span>
|
||||
<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 className={styles.sessionBadges}>
|
||||
@ -891,11 +891,11 @@ export default function POSPage() {
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
<div className={styles.propGroup}>
|
||||
<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 className={styles.propGroup}>
|
||||
<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 className={styles.propGroup}>
|
||||
<label>Shape</label>
|
||||
|
||||
@ -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 {
|
||||
|
||||
472
src/app/restaurant/orders/orders.module.css
Normal file
472
src/app/restaurant/orders/orders.module.css
Normal 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);
|
||||
}
|
||||
275
src/app/restaurant/orders/page.tsx
Normal file
275
src/app/restaurant/orders/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -38,20 +38,61 @@ const initialTables = [
|
||||
];
|
||||
|
||||
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 [tables, setTables] = useState(initialTables);
|
||||
const [selectedTable, setSelectedTable] = useState<number | null>(null);
|
||||
const [user, setUser] = useState<{ name: string; role: string } | null>(null);
|
||||
const [tables, setTables] = useState<any[]>([]);
|
||||
const [selectedTableId, setSelectedTableId] = useState<number | 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(() => {
|
||||
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 <div style={{ padding: '2rem', color: 'white' }}>Loading Floor Plan...</div>;
|
||||
|
||||
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}>
|
||||
{/* Floor Navigation */}
|
||||
<div className={styles.floorNav}>
|
||||
{['Main Floor', 'Terrace', 'Bar'].map(floor => (
|
||||
<button
|
||||
key={floor}
|
||||
className={`${styles.floorBtn} ${activeFloor === floor ? styles.activeFloor : ''}`}
|
||||
onClick={() => setActiveFloor(floor)}
|
||||
>
|
||||
{floor}
|
||||
</button>
|
||||
))}
|
||||
<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
|
||||
{currentView === 'Plan' && (
|
||||
<>
|
||||
{/* Floor Navigation */}
|
||||
<div className={styles.floorNav}>
|
||||
{floors.map(floor => (
|
||||
<button
|
||||
key={floor.id}
|
||||
className={`${styles.floorBtn} ${activeFloorId === floor.id ? styles.activeFloor : ''}`}
|
||||
onClick={() => setActiveFloorId(floor.id)}
|
||||
>
|
||||
{floor.name}
|
||||
</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>
|
||||
|
||||
{/* Main Floor Area */}
|
||||
<div className={styles.floorPlan}>
|
||||
{!isEditing && (
|
||||
<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: '#ef4444' }}></span> Occupied</div>
|
||||
<div className={styles.legendItem}><span className={styles.statusDot} style={{ backgroundColor: '#f59e0b' }}></span> Reserved</div>
|
||||
{/* Main Floor Area */}
|
||||
<div className={styles.floorPlan}>
|
||||
{!isEditing && (
|
||||
<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: '#ef4444' }}></span> Occupied</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 className={styles.canvas}>
|
||||
{tables.map(table => (
|
||||
<motion.div
|
||||
key={table.id}
|
||||
drag={isEditing}
|
||||
dragMomentum={false}
|
||||
onDrag={(e, info) => 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 ? (
|
||||
<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>
|
||||
{/* Designer Sidebar / KDS Sidebar */}
|
||||
<div className={styles.rightSidebar}>
|
||||
{isEditing && selectedTableId ? (
|
||||
<div className={styles.propertyEditor}>
|
||||
<div className={styles.sidebarHeader}>
|
||||
<h3>Table Properties</h3>
|
||||
<button className={styles.deleteBtn} onClick={() => deleteTable(selectedTableId)}>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
<div className={styles.propGroup}>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Designer Sidebar / KDS Sidebar */}
|
||||
<div className={styles.rightSidebar}>
|
||||
{isEditing && selectedTable ? (
|
||||
<div className={styles.propertyEditor}>
|
||||
<div className={styles.sidebarHeader}>
|
||||
<h3>Table Properties</h3>
|
||||
<button className={styles.deleteBtn} onClick={() => deleteTable(selectedTable)}>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
{currentView === 'Graph' && (
|
||||
<div className={styles.graphView}>
|
||||
<div className={styles.chartCard} style={{ flex: 2 }}>
|
||||
<h3>Table Occupancy</h3>
|
||||
<div style={{ height: '300px', display: 'flex', alignItems: 'flex-end', gap: '2rem' }}>
|
||||
{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 (
|
||||
<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 className={styles.propGroup}>
|
||||
<label>Identifier</label>
|
||||
<label>Floor Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentTable?.name}
|
||||
onChange={(e) => updateTable(selectedTable, { name: e.target.value })}
|
||||
placeholder="e.g. Roof Top, Garden"
|
||||
value={newFloorName}
|
||||
onChange={(e) => setNewFloorName(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.propGroup}>
|
||||
<label>Seats</label>
|
||||
<input
|
||||
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 className={styles.modalFooter}>
|
||||
<button className={styles.deleteBtn} style={{ background: '#f1f5f9', color: '#64748b', borderColor: '#e2e8f0' }} onClick={() => setIsFloorModalOpen(false)}>Cancel</button>
|
||||
<button className={styles.addTableBtn} onClick={addFloor}>Create Floor</button>
|
||||
</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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
<div className={styles.navLeft}>
|
||||
<span className={styles.appTitle}>{title}</span>
|
||||
<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 className={styles.navCenter}>
|
||||
<div className={styles.searchBar}>
|
||||
<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>
|
||||
|
||||
@ -113,14 +119,21 @@ export default function AppLayout({ children, title, sidebarItems }: AppLayoutPr
|
||||
{/* Action Bar */}
|
||||
<div className={styles.actionBar}>
|
||||
<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} onClick={() => window.print()}>Print</button>
|
||||
</div>
|
||||
<div className={styles.actionRight}>
|
||||
<div className={styles.viewToggle}>
|
||||
<button className={styles.viewBtn}>List</button>
|
||||
<button className={styles.viewBtn}>Kanban</button>
|
||||
<button className={styles.viewBtn}>Graph</button>
|
||||
{['Plan', 'List', 'Kanban', 'Graph'].map(view => (
|
||||
<button
|
||||
key={view}
|
||||
className={`${styles.viewBtn} ${currentView === view ? styles.activeView : ''}`}
|
||||
onClick={() => onViewChange && onViewChange(view)}
|
||||
>
|
||||
{view}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user