first commit

This commit is contained in:
Alaguraj0361 2026-01-22 21:28:51 +05:30
commit b7e599d0b2
54 changed files with 15903 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

18
eslint.config.mjs Normal file
View File

@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

7
next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

7811
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "odoo-next-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@stripe/react-stripe-js": "^5.4.1",
"@stripe/stripe-js": "^8.6.3",
"bcryptjs": "^3.0.3",
"clsx": "^2.1.1",
"cookies-next": "^6.1.1",
"framer-motion": "^12.27.5",
"jsonwebtoken": "^9.0.3",
"lucide-react": "^0.562.0",
"mongoose": "^9.1.5",
"next": "16.1.4",
"nodemailer": "^7.0.12",
"react": "19.2.3",
"react-dom": "19.2.3",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/nodemailer": "^7.0.5",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.4",
"typescript": "^5"
}
}

1
public/file.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@ -0,0 +1,161 @@
.container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.header {
display: flex;
flex-direction: column;
gap: 2rem;
}
.statsRow {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.statBox {
background: white;
padding: 1.5rem;
border-radius: 20px;
border: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
gap: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.statLabel {
font-size: 0.8rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
}
.statValue {
font-size: 1.5rem;
font-weight: 800;
color: #1e293b;
font-family: var(--font-heading);
}
.actions {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.searchBox {
flex: 1;
max-width: 400px;
position: relative;
display: flex;
align-items: center;
}
.searchBox svg {
position: absolute;
left: 12px;
color: #94a3b8;
}
.searchBox input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.5rem;
background: white;
border: 1px solid #e2e8f0;
border-radius: 12px;
outline: none;
transition: var(--transition);
}
.addBtn {
background: var(--pk-primary);
color: white;
border: none;
padding: 0.75rem 1.25rem;
border-radius: 12px;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.exportBtn {
background: white;
border: 1px solid #e2e8f0;
padding: 0.75rem;
border-radius: 12px;
color: #64748b;
cursor: pointer;
}
.tableWrapper {
background: white;
border-radius: 20px;
border: 1px solid #e2e8f0;
overflow: hidden;
box-shadow: var(--shadow);
}
.ledgerTable {
width: 100%;
border-collapse: collapse;
text-align: left;
}
.ledgerTable th {
padding: 1rem 1.5rem;
background: #f8fafc;
color: #64748b;
font-weight: 700;
font-size: 0.85rem;
border-bottom: 1px solid #e2e8f0;
}
.ledgerTable td {
padding: 1rem 1.5rem;
border-bottom: 1px solid #f1f5f9;
font-size: 0.9rem;
color: #1e293b;
}
.row:hover {
background: #f8fafc;
}
.entryNumber {
font-weight: 700;
color: var(--pk-primary);
}
.amount {
font-weight: 700;
font-family: var(--font-heading);
}
.statusBadge {
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 700;
}
.posted {
background: #dcfce7;
color: #15803d;
}
.draft {
background: #fef9c3;
color: #854d0e;
}
.moreIcon {
color: #94a3b8;
cursor: pointer;
}

110
src/app/accounting/page.tsx Normal file
View File

@ -0,0 +1,110 @@
"use client";
import React, { useState } from 'react';
import AppLayout from '@/components/AppLayout/AppLayout';
import {
BookOpen,
ArrowLeftRight,
FileText,
Settings,
Plus,
Search,
Filter,
Download,
MoreHorizontal,
LayoutDashboard,
Wallet,
Receipt
} from 'lucide-react';
import styles from './accounting.module.css';
const sidebarItems = [
{ icon: <LayoutDashboard size={20} />, label: 'Accounting Dashboard' },
{ icon: <BookOpen size={20} />, label: 'Journal Entries', active: true },
{ icon: <Receipt size={20} />, label: 'Vendor Bills' },
{ icon: <ArrowLeftRight size={20} />, label: 'Bank Sync' },
];
const journalEntries = [
{ id: 'BNK1/2026/001', date: '2026-01-20', journal: 'Bank', partner: 'Azure Interior', reference: 'INV/2026/001', amount: '$ 1,250.00', status: 'Posted' },
{ id: 'INV/2026/042', date: '2026-01-21', journal: 'Customer Invoices', partner: 'Deco Addict', reference: 'Sales Order S0002', amount: '$ 4,500.00', status: 'Draft' },
{ id: 'BILL/2026/012', date: '2026-01-21', journal: 'Vendor Bills', partner: 'Fresh Produce Co.', reference: 'Monthly Supply', amount: '$ -850.00', status: 'Posted' },
{ id: 'MISC/2026/005', date: '2026-01-22', journal: 'Miscellaneous', partner: '', reference: 'Year End Adjustment', amount: '$ 0.00', status: 'Posted' },
];
export default function AccountingPage() {
const [search, setSearch] = useState('');
return (
<AppLayout title="Accounting Ledger" sidebarItems={sidebarItems}>
<div className={styles.container}>
{/* Ledger Header */}
<div className={styles.header}>
<div className={styles.statsRow}>
<div className={styles.statBox}>
<span className={styles.statLabel}>Total Assets</span>
<span className={styles.statValue}>$ 245,600.00</span>
</div>
<div className={styles.statBox}>
<span className={styles.statLabel}>Accounts Receivable</span>
<span className={styles.statValue}>$ 12,450.00</span>
</div>
<div className={styles.statBox}>
<span className={styles.statLabel}>Accounts Payable</span>
<span className={styles.statValue} style={{ color: '#ef4444' }}>$ -4,200.00</span>
</div>
</div>
<div className={styles.actions}>
<div className={styles.searchBox}>
<Search size={18} />
<input type="text" placeholder="Search entries..." value={search} onChange={(e) => setSearch(e.target.value)} />
</div>
<button className={styles.addBtn}><Plus size={18} /> New Entry</button>
<button className={styles.exportBtn}><Download size={18} /></button>
</div>
</div>
{/* Ledger Table */}
<div className={styles.tableWrapper}>
<table className={styles.ledgerTable}>
<thead>
<tr>
<th><input type="checkbox" /></th>
<th>Date</th>
<th>Number</th>
<th>Journal</th>
<th>Partner</th>
<th>Reference</th>
<th>Amount</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{journalEntries.map((entry) => (
<tr key={entry.id} className={styles.row}>
<td><input type="checkbox" /></td>
<td>{entry.date}</td>
<td className={styles.entryNumber}>{entry.id}</td>
<td>{entry.journal}</td>
<td>{entry.partner || '-'}</td>
<td>{entry.reference}</td>
<td className={styles.amount} style={{ color: entry.amount.startsWith('$ -') ? '#ef4444' : '#1e293b' }}>
{entry.amount}
</td>
<td>
<span className={`${styles.statusBadge} ${styles[entry.status.toLowerCase()]}`}>
{entry.status}
</span>
</td>
<td><MoreHorizontal size={18} className={styles.moreIcon} /></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</AppLayout>
);
}

View File

@ -0,0 +1,47 @@
import { NextResponse } from 'next/server';
import dbConnect from '@/lib/mongodb';
import User from '@/models/User';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'your_super_secret_jwt_key';
export async function POST(req: Request) {
try {
const { email, password } = await req.json();
await dbConnect();
const user = await User.findOne({ email });
if (!user) {
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}
const token = jwt.sign(
{ userId: user._id, role: user.role, name: user.name },
JWT_SECRET,
{ expiresIn: '1d' }
);
const response = NextResponse.json({
message: 'Login successful',
user: { id: user._id, name: user.name, email: user.email, role: user.role }
});
response.cookies.set('auth_token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 86400,
path: '/',
});
return response;
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -0,0 +1,28 @@
import { NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import { cookies } from 'next/headers';
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;
if (!token) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
const decoded = jwt.verify(token, JWT_SECRET) as any;
return NextResponse.json({
user: {
id: decoded.userId,
name: decoded.name,
role: decoded.role,
}
});
} catch (error) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
}
}

View File

@ -0,0 +1,63 @@
import { NextResponse } from 'next/server';
import dbConnect from '@/lib/mongodb';
import User from '@/models/User';
import crypto from 'crypto';
export async function POST(req: Request) {
try {
const { email } = await req.json();
await dbConnect();
const user = await User.findOne({ email });
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
const resetToken = crypto.randomBytes(32).toString('hex');
const resetTokenExpire = new Date(Date.now() + 3600000); // 1 hour
user.resetPasswordToken = resetToken;
user.resetPasswordExpires = resetTokenExpire;
await user.save();
// In a real app, you would send an email here using nodemailer
// console.log(`Reset token: ${resetToken}`);
return NextResponse.json({
message: 'Password reset link sent to your email (simulated)',
token: resetToken // Only returning for demonstration purposes
});
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function PUT(req: Request) {
try {
const { token, newPassword } = await req.json();
await dbConnect();
const user = await User.findOne({
resetPasswordToken: token,
resetPasswordExpires: { $gt: Date.now() }
});
if (!user) {
return NextResponse.json({ error: 'Invalid or expired token' }, { status: 400 });
}
const bcrypt = require('bcryptjs');
const hashedPassword = await bcrypt.hash(newPassword, 12);
user.password = hashedPassword;
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
await user.save();
return NextResponse.json({ message: 'Password has been reset successfully' });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -0,0 +1,33 @@
import { NextResponse } from 'next/server';
import dbConnect from '@/lib/mongodb';
import User from '@/models/User';
import bcrypt from 'bcryptjs';
export async function POST(req: Request) {
try {
const { name, email, password } = await req.json();
await dbConnect();
const existingUser = await User.findOne({ email });
if (existingUser) {
return NextResponse.json({ error: 'Email already in use' }, { status: 400 });
}
const hashedPassword = await bcrypt.hash(password, 12);
const user = await User.create({
name,
email,
password: hashedPassword,
role: 'admin', // Default first user as admin
});
return NextResponse.json({
message: 'User created successfully',
user: { id: user._id, name: user.name, email: user.email }
}, { status: 201 });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -0,0 +1,218 @@
.authPage {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle at top left, rgba(113, 75, 103, 0.05) 0%, transparent 40%),
radial-gradient(circle at bottom right, rgba(1, 126, 132, 0.05) 0%, transparent 40%),
var(--bg-main);
padding: 2rem;
}
.authCard {
width: 100%;
max-width: 440px;
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
border: 1px solid var(--glass-border);
padding: 3rem;
border-radius: 24px;
box-shadow: var(--shadow-lg);
display: flex;
flex-direction: column;
gap: 2rem;
}
.logoSection {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
}
.logoIcon {
width: 40px;
height: 40px;
background: white;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: var(--pk-primary);
font-weight: 800;
font-size: 1.25rem;
box-shadow: var(--shadow);
}
.logoSection h1 {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-main);
}
.logoSection h1 span {
font-weight: 400;
color: var(--text-muted);
}
.header {
text-align: center;
}
.header h2 {
font-size: 1.5rem;
color: var(--text-main);
margin-bottom: 0.5rem;
}
.header p {
color: var(--text-muted);
font-size: 0.95rem;
}
.errorMessage {
background: #fee2e2;
color: #b91c1c;
padding: 0.75rem 1rem;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 500;
border: 1px solid rgba(185, 28, 28, 0.1);
}
.form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.inputGroup {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.labelRow {
display: flex;
justify-content: space-between;
align-items: center;
}
.inputGroup label {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-main);
display: flex;
align-items: center;
gap: 0.5rem;
}
.forgotPass {
font-size: 0.8rem;
color: var(--pk-primary);
text-decoration: none;
font-weight: 600;
}
.inputGroup input {
padding: 0.8rem 1rem;
background: white;
border: 1px solid #e2e8f0;
border-radius: 12px;
outline: none;
transition: var(--transition);
font-family: var(--font-sans);
}
.inputGroup input:focus {
border-color: var(--pk-primary);
box-shadow: 0 0 0 4px rgba(113, 75, 103, 0.1);
}
.submitBtn {
margin-top: 1rem;
padding: 1rem;
background: var(--pk-primary);
color: white;
border: none;
border-radius: 14px;
font-weight: 700;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
cursor: pointer;
transition: var(--transition);
}
.successMessage {
background: #dcfce7;
color: #15803d;
padding: 0.75rem 1rem;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
border: 1px solid rgba(21, 128, 61, 0.1);
}
.backToLogin {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 1rem;
color: var(--text-muted);
text-decoration: none;
font-size: 0.9rem;
font-weight: 600;
transition: var(--transition);
}
.backToLogin:hover {
color: var(--pk-primary);
}
.submitBtn:hover:not(:disabled) {
background: var(--pk-primary-hover);
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(113, 75, 103, 0.3);
}
.submitBtn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.spinner {
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.switchAuth {
text-align: center;
font-size: 0.9rem;
color: var(--text-muted);
}
.switchAuth a {
color: var(--pk-primary);
text-decoration: none;
font-weight: 700;
}
.switchAuth a:hover {
text-decoration: underline;
}

View File

@ -0,0 +1,98 @@
"use client";
import React, { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { LogIn, Mail, Lock, Loader2 } from 'lucide-react';
import { setCookie } from 'cookies-next';
import styles from '../auth.module.css';
export default function LoginPage() {
const [formData, setFormData] = useState({ email: '', password: '' });
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const res = await fetch('http://localhost:5000/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Login failed');
// Set token in cookies for the frontend to use
setCookie('auth_token', data.token, { maxAge: 86400, path: '/' });
// Store user info in localStorage for quick access
localStorage.setItem('user', JSON.stringify(data.user));
router.push('/');
router.refresh();
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div className={styles.authPage}>
<div className={styles.authCard}>
<div className={styles.logoSection}>
<div className={styles.logoIcon}>O</div>
<h1>Odoo<span>Next</span></h1>
</div>
<div className={styles.header}>
<h2>Welcome Back</h2>
<p>Login to manage your business</p>
</div>
{error && <div className={styles.errorMessage}>{error}</div>}
<form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.inputGroup}>
<label><Mail size={16} /> Email Address</label>
<input
type="email"
placeholder="name@company.com"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
</div>
<div className={styles.inputGroup}>
<div className={styles.labelRow}>
<label><Lock size={16} /> Password</label>
<Link href="/auth/reset-password" className={styles.forgotPass}>Forgot?</Link>
</div>
<input
type="password"
placeholder="••••••••"
required
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
</div>
<button type="submit" className={styles.submitBtn} disabled={loading}>
{loading ? <Loader2 className={styles.spinner} size={20} /> : <><LogIn size={20} /> Login</>}
</button>
</form>
<p className={styles.switchAuth}>
Don't have an account? <Link href="/auth/signup">Sign Up</Link>
</p>
</div>
</div>
);
}

View File

@ -0,0 +1,160 @@
"use client";
import React, { useState, useEffect, Suspense } from 'react';
import Link from 'next/link';
import { useSearchParams, useRouter } from 'next/navigation';
import { Mail, Loader2, ArrowLeft, ShieldCheck, Lock } from 'lucide-react';
import styles from '../auth.module.css';
function ResetPasswordForm() {
const searchParams = useSearchParams();
const router = useRouter();
const token = searchParams.get('token');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
// If no token, we show the "Forgot Password" (request link) UI
// If token exists, we show the "Set New Password" UI
const isResetting = !!token;
const handleRequestLink = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
setMessage('');
try {
const res = await fetch('http://localhost:5000/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to send reset link');
setMessage(data.message);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleResetPassword = async (e: React.FormEvent) => {
e.preventDefault();
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
setLoading(true);
setError('');
setMessage('');
try {
const res = await fetch('http://localhost:5000/api/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, password }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Reset failed');
setMessage('Password reset successful! Redirecting to login...');
setTimeout(() => router.push('/auth/login'), 2000);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div className={styles.authPage}>
<div className={styles.authCard}>
<div className={styles.logoSection}>
<div className={styles.logoIcon}>O</div>
<h1>Odoo<span>Next</span></h1>
</div>
<div className={styles.header}>
<h2>{isResetting ? 'Set New Password' : 'Reset Password'}</h2>
<p>{isResetting ? 'Enter your new password below' : "We'll send you a link to get back into your account"}</p>
</div>
{error && <div className={styles.errorMessage}>{error}</div>}
{message && (
<div className={styles.successMessage}>
<ShieldCheck size={20} />
{message}
</div>
)}
{!isResetting ? (
<form className={styles.form} onSubmit={handleRequestLink}>
<div className={styles.inputGroup}>
<label><Mail size={16} /> Email Address</label>
<input
type="email"
placeholder="name@company.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<button type="submit" className={styles.submitBtn} disabled={loading}>
{loading ? <Loader2 className={styles.spinner} size={20} /> : 'Send Reset Link'}
</button>
</form>
) : (
<form className={styles.form} onSubmit={handleResetPassword}>
<div className={styles.inputGroup}>
<label><Lock size={16} /> New Password</label>
<input
type="password"
placeholder="••••••••"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className={styles.inputGroup}>
<label><Lock size={16} /> Confirm Password</label>
<input
type="password"
placeholder="••••••••"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<button type="submit" className={styles.submitBtn} disabled={loading}>
{loading ? <Loader2 className={styles.spinner} size={20} /> : 'Update Password'}
</button>
</form>
)}
<Link href="/auth/login" className={styles.backToLogin}>
<ArrowLeft size={16} /> Back to Login
</Link>
</div>
</div>
);
}
export default function ResetPasswordPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ResetPasswordForm />
</Suspense>
);
}

View File

@ -0,0 +1,104 @@
"use client";
import React, { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { UserPlus, Mail, Lock, User as UserIcon, Loader2 } from 'lucide-react';
import { setCookie } from 'cookies-next';
import styles from '../auth.module.css';
export default function SignupPage() {
const [formData, setFormData] = useState({ name: '', email: '', password: '' });
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const res = await fetch('http://localhost:5000/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Signup failed');
// Set token and user data similar to login
setCookie('auth_token', data.token, { maxAge: 86400, path: '/' });
localStorage.setItem('user', JSON.stringify(data.user));
router.push('/');
router.refresh();
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div className={styles.authPage}>
<div className={styles.authCard}>
<div className={styles.logoSection}>
<div className={styles.logoIcon}>O</div>
<h1>Odoo<span>Next</span></h1>
</div>
<div className={styles.header}>
<h2>Create Account</h2>
<p>Get started with your free ERP trial</p>
</div>
{error && <div className={styles.errorMessage}>{error}</div>}
<form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.inputGroup}>
<label><UserIcon size={16} /> Full Name</label>
<input
type="text"
placeholder="John Doe"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className={styles.inputGroup}>
<label><Mail size={16} /> Email Address</label>
<input
type="email"
placeholder="name@company.com"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
</div>
<div className={styles.inputGroup}>
<label><Lock size={16} /> Password</label>
<input
type="password"
placeholder="••••••••"
required
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
</div>
<button type="submit" className={styles.submitBtn} disabled={loading}>
{loading ? <Loader2 className={styles.spinner} size={20} /> : <><UserPlus size={20} /> Create Account</>}
</button>
</form>
<p className={styles.switchAuth}>
Already have an account? <Link href="/auth/login">Login</Link>
</p>
</div>
</div>
);
}

151
src/app/crm/crm.module.css Normal file
View File

@ -0,0 +1,151 @@
.kanbanBoard {
display: flex;
gap: 1.5rem;
height: calc(100vh - 180px);
overflow-x: auto;
padding-bottom: 1rem;
}
.column {
min-width: 300px;
max-width: 300px;
background: #f1f5f9;
border-radius: 12px;
display: flex;
flex-direction: column;
}
.columnHeader {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.columnTitle {
display: flex;
align-items: center;
justify-content: space-between;
}
.columnName {
font-weight: 700;
color: #1e293b;
font-size: 0.95rem;
text-transform: uppercase;
letter-spacing: 0.025em;
}
.columnBadge {
background: #cbd5e1;
color: #475569;
font-size: 0.75rem;
padding: 0.1rem 0.5rem;
border-radius: 999px;
font-weight: 600;
}
.columnTotal {
font-size: 0.85rem;
color: #64748b;
font-weight: 500;
}
.columnContent {
flex: 1;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
overflow-y: auto;
}
.card {
background: white;
border-radius: 8px;
padding: 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid transparent;
transition: var(--transition);
cursor: grab;
}
.card:hover {
border-color: var(--pk-primary);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.cardHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.cardTitle {
font-weight: 600;
color: #1e293b;
font-size: 0.9rem;
line-height: 1.25;
}
.cardActions {
color: #94a3b8;
cursor: pointer;
}
.companyName {
font-size: 0.8rem;
color: #64748b;
margin-bottom: 0.75rem;
}
.cardFooter {
display: flex;
justify-content: space-between;
align-items: center;
}
.cardValue {
font-weight: 700;
color: var(--pk-secondary);
font-size: 0.85rem;
}
.priority {
display: flex;
gap: 2px;
}
.star {
color: #e2e8f0;
font-size: 14px;
}
.starActive {
color: #f59e0b;
font-size: 14px;
}
.addCardBtn {
width: 100%;
padding: 0.6rem;
background: transparent;
border: 1px dashed #cbd5e1;
border-radius: 8px;
color: #64748b;
font-size: 0.85rem;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
cursor: pointer;
transition: var(--transition);
}
.addCardBtn:hover {
background: white;
color: var(--pk-primary);
border-color: var(--pk-primary);
}

105
src/app/crm/page.tsx Normal file
View File

@ -0,0 +1,105 @@
"use client";
import React from 'react';
import AppLayout from '@/components/AppLayout/AppLayout';
import {
Users,
LayoutDashboard,
Calendar,
MessageSquare,
MoreVertical,
Plus
} from 'lucide-react';
import styles from './crm.module.css';
const sidebarItems = [
{ icon: <LayoutDashboard size={20} />, label: 'Dashboard' },
{ icon: <Users size={20} />, label: 'Pipelines', active: true },
{ icon: <Calendar size={20} />, label: 'Activities' },
{ icon: <MessageSquare size={20} />, label: 'Leads' },
];
const kanbanColumns = [
{
id: 'new',
name: 'New',
count: 3,
total: '$ 12,500',
items: [
{ id: '1', title: 'Plan for new office', company: 'Azure Interior', value: '$ 5,000', priority: 2 },
{ id: '2', title: 'Consultation for kitchen', company: 'Deco Addict', value: '$ 2,500', priority: 1 },
{ id: '3', title: 'Interior design project', company: 'Ready Mat', value: '$ 5,000', priority: 3 },
]
},
{
id: 'qualified',
name: 'Qualified',
count: 2,
total: '$ 45,000',
items: [
{ id: '4', title: 'Full house renovation', company: 'Gemini Corp', value: '$ 30,000', priority: 3 },
{ id: '5', title: 'Office furniture setup', company: 'The Corner', value: '$ 15,000', priority: 2 },
]
},
{
id: 'proposition',
name: 'Proposition',
count: 1,
total: '$ 8,000',
items: [
{ id: '6', title: 'Smart home automation', company: 'Lumiere', value: '$ 8,000', priority: 3 },
]
},
{
id: 'won',
name: 'Won',
count: 0,
total: '$ 0',
items: []
},
];
export default function CRMPage() {
return (
<AppLayout title="CRM" sidebarItems={sidebarItems}>
<div className={styles.kanbanBoard}>
{kanbanColumns.map((column) => (
<div key={column.id} className={styles.column}>
<div className={styles.columnHeader}>
<div className={styles.columnTitle}>
<span className={styles.columnName}>{column.name}</span>
<span className={styles.columnBadge}>{column.count}</span>
</div>
<div className={styles.columnTotal}>{column.total}</div>
</div>
<div className={styles.columnContent}>
{column.items.map((item) => (
<div key={item.id} className={styles.card}>
<div className={styles.cardHeader}>
<span className={styles.cardTitle}>{item.title}</span>
<MoreVertical size={14} className={styles.cardActions} />
</div>
<div className={styles.cardBody}>
<div className={styles.companyName}>{item.company}</div>
<div className={styles.cardFooter}>
<div className={styles.cardValue}>{item.value}</div>
<div className={styles.priority}>
{[...Array(3)].map((_, i) => (
<span key={i} className={i < item.priority ? styles.starActive : styles.star}></span>
))}
</div>
</div>
</div>
</div>
))}
<button className={styles.addCardBtn}>
<Plus size={16} /> Add a card
</button>
</div>
</div>
))}
</div>
</AppLayout>
);
}

View File

@ -0,0 +1,180 @@
.dashboardGrid {
display: flex;
flex-direction: column;
gap: 2rem;
}
.kpiRow {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.kpiCard {
background: white;
padding: 1.5rem;
border-radius: 16px;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
transition: var(--transition);
}
.kpiCard:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.kpiHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.kpiIcon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.change {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.85rem;
font-weight: 700;
padding: 0.25rem 0.5rem;
border-radius: 6px;
}
.up {
background: #dcfce7;
color: #15803d;
}
.down {
background: #fee2e2;
color: #b91c1c;
}
.kpiValue {
font-size: 2rem;
font-weight: 800;
color: #1e293b;
margin-bottom: 0.25rem;
font-family: var(--font-heading);
}
.kpiLabel {
color: #64748b;
font-weight: 500;
font-size: 0.95rem;
}
.contentRow {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 1.5rem;
}
.sectionHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.sectionHeader h3 {
font-size: 1.1rem;
font-weight: 700;
color: #1e293b;
}
.moreBtn {
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
}
.chartSection,
.activitySection {
background: white;
padding: 1.5rem;
border-radius: 16px;
border: 1px solid #e2e8f0;
}
.chartPlaceholder {
height: 250px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.svgChart {
width: 100%;
height: 180px;
}
.chartLabels {
display: flex;
justify-content: space-between;
color: #94a3b8;
font-size: 0.8rem;
font-weight: 500;
padding: 0 10px;
}
.activityList {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.activityItem {
display: flex;
align-items: center;
gap: 1rem;
}
.activityUser {
width: 36px;
height: 36px;
background: #f1f5f9;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: var(--pk-primary);
font-size: 0.8rem;
}
.activityInfo {
flex: 1;
}
.activityInfo p {
font-size: 0.9rem;
color: #1e293b;
}
.activityInfo span {
font-size: 0.8rem;
color: #64748b;
}
.activityTime {
font-size: 0.75rem;
color: #94a3b8;
}
@media (max-width: 1024px) {
.contentRow {
grid-template-columns: 1fr;
}
}

116
src/app/dashboard/page.tsx Normal file
View File

@ -0,0 +1,116 @@
"use client";
import React from 'react';
import AppLayout from '@/components/AppLayout/AppLayout';
import {
TrendingUp,
Users,
ShoppingCart,
DollarSign,
ArrowUpRight,
ArrowDownRight,
Clock,
MoreVertical
} from 'lucide-react';
import styles from './dashboard.module.css';
const sidebarItems = [
{ icon: <TrendingUp size={20} />, label: 'Overview', active: true },
{ icon: <Users size={20} />, label: 'Analytics' },
{ icon: <Clock size={20} />, label: 'History' },
];
const kpis = [
{ label: 'Total Revenue', value: '$ 124,500', change: '+12.5%', isUp: true, icon: <DollarSign size={24} />, color: '#714B67' },
{ label: 'Sales Orders', value: '1,420', change: '+8.2%', isUp: true, icon: <ShoppingCart size={24} />, color: '#017E84' },
{ label: 'New Customers', value: '384', change: '-2.4%', isUp: false, icon: <Users size={24} />, color: '#4B7BEC' },
];
const activities = [
{ id: 1, user: 'James Doe', action: 'Confirmed Sales Order', detail: 'S00042', time: '2 mins ago' },
{ id: 2, user: 'Alice Smith', action: 'Created New Lead', detail: 'Azure Interior', time: '15 mins ago' },
{ id: 3, user: 'System', action: 'Inventory Alert', detail: 'Low stock on Office Chair', time: '1 hour ago' },
{ id: 4, user: 'Bob Wilson', action: 'Payment Received', detail: '$ 450.00 from Deco Addict', time: '3 hours ago' },
];
export default function DashboardPage() {
return (
<AppLayout title="Dashboard" sidebarItems={sidebarItems}>
<div className={styles.dashboardGrid}>
{/* KPI Row */}
<section className={styles.kpiRow}>
{kpis.map((kpi, idx) => (
<div key={idx} className={styles.kpiCard}>
<div className={styles.kpiHeader}>
<div className={styles.kpiIcon} style={{ backgroundColor: `${kpi.color}15`, color: kpi.color }}>
{kpi.icon}
</div>
<div className={`${styles.change} ${kpi.isUp ? styles.up : styles.down}`}>
{kpi.isUp ? <ArrowUpRight size={14} /> : <ArrowDownRight size={14} />}
{kpi.change}
</div>
</div>
<div className={styles.kpiValue}>{kpi.value}</div>
<div className={styles.kpiLabel}>{kpi.label}</div>
</div>
))}
</section>
{/* Charts & Reports */}
<div className={styles.contentRow}>
<section className={styles.chartSection}>
<div className={styles.sectionHeader}>
<h3>Revenue Growth</h3>
<button className={styles.moreBtn}><MoreVertical size={18} /></button>
</div>
<div className={styles.chartPlaceholder}>
{/* Fake Chart SVG */}
<svg viewBox="0 0 400 150" className={styles.svgChart}>
<path
d="M0 120 Q 50 110, 100 130 T 200 80 T 300 100 T 400 40"
fill="none"
stroke="var(--pk-primary)"
strokeWidth="3"
strokeLinecap="round"
/>
<path
d="M0 150 L 0 120 Q 50 110, 100 130 T 200 80 T 300 100 T 400 40 L 400 150 Z"
fill="url(#gradient)"
opacity="0.1"
/>
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="var(--pk-primary)" />
<stop offset="100%" stopColor="transparent" />
</linearGradient>
</defs>
</svg>
<div className={styles.chartLabels}>
<span>Jan</span><span>Feb</span><span>Mar</span><span>Apr</span><span>May</span><span>Jun</span>
</div>
</div>
</section>
<section className={styles.activitySection}>
<div className={styles.sectionHeader}>
<h3>Recent Activity</h3>
<button className={styles.moreBtn}><MoreVertical size={18} /></button>
</div>
<div className={styles.activityList}>
{activities.map(activity => (
<div key={activity.id} className={styles.activityItem}>
<div className={styles.activityUser}>{activity.user[0]}</div>
<div className={styles.activityInfo}>
<p><strong>{activity.user}</strong> {activity.action}</p>
<span>{activity.detail}</span>
</div>
<div className={styles.activityTime}>{activity.time}</div>
</div>
))}
</div>
</section>
</div>
</div>
</AppLayout>
);
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,269 @@
.container {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.titleArea h2 {
font-size: 1.75rem;
font-weight: 800;
color: #1e293b;
}
.titleArea p {
color: #64748b;
font-size: 1rem;
}
.headerActions {
display: flex;
gap: 1rem;
}
.dateSelector,
.exportBtn {
padding: 0.75rem 1.25rem;
border-radius: 12px;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
transition: var(--transition);
}
.dateSelector {
background: white;
border: 1px solid #e2e8f0;
color: #1e293b;
}
.exportBtn {
background: #1e293b;
color: white;
border: none;
}
.metricsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.5rem;
}
.metricCard {
background: white;
padding: 1.5rem;
border-radius: 20px;
border: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
gap: 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.metricLabel {
font-size: 0.85rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.025em;
}
.metricMain {
display: flex;
align-items: baseline;
justify-content: space-between;
}
.metricValue {
font-size: 1.75rem;
font-weight: 800;
color: #1e293b;
font-family: var(--font-heading);
}
.badge {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
font-weight: 700;
padding: 4px 8px;
border-radius: 999px;
}
.up {
background: #dcfce7;
color: #15803d;
}
.down {
background: #fee2e2;
color: #b91c1c;
}
.miniChart {
width: 100%;
height: 6px;
background: #f1f5f9;
border-radius: 10px;
overflow: hidden;
margin-top: 0.5rem;
}
.progress {
height: 100%;
border-radius: 10px;
}
.contentRow {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
}
.chartCard,
.topItemsCard {
background: white;
padding: 2rem;
border-radius: 24px;
border: 1px solid #e2e8f0;
}
.sectionHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.sectionHeader h3 {
font-size: 1.1rem;
font-weight: 800;
color: #1e293b;
}
.legend {
display: flex;
gap: 1rem;
font-size: 0.8rem;
font-weight: 700;
}
.revenueL::before {
content: '';
display: inline-block;
width: 10px;
height: 10px;
background: #714b67;
border-radius: 50%;
margin-right: 6px;
}
.costL::before {
content: '';
display: inline-block;
width: 10px;
height: 10px;
background: #ef4444;
border-radius: 50%;
margin-right: 6px;
}
.visualPlaceholder {
height: 250px;
display: flex;
flex-direction: column;
}
.svgChart {
flex: 1;
}
.chartLabels {
display: flex;
justify-content: space-between;
color: #94a3b8;
font-size: 0.75rem;
font-weight: 700;
padding: 1rem 0 0 0;
}
.itemsList {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.itemRow {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 1rem;
border-bottom: 1px solid #f1f5f9;
}
.itemInfo {
display: flex;
flex-direction: column;
}
.itemInfo strong {
font-size: 0.95rem;
color: #1e293b;
}
.itemInfo span {
font-size: 0.8rem;
color: #64748b;
}
.itemStats {
display: flex;
flex-direction: column;
text-align: right;
}
.itemRevenue {
font-weight: 800;
color: #1e293b;
}
.itemMargin {
font-size: 0.75rem;
color: #10b981;
font-weight: 700;
}
.viewDetailed {
margin-top: 2rem;
width: 100%;
padding: 1rem;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 12px;
font-weight: 700;
color: var(--pk-primary);
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
cursor: pointer;
transition: var(--transition);
}
.viewDetailed:hover {
background: #f1f5f9;
}
@media (max-width: 1024px) {
.contentRow {
grid-template-columns: 1fr;
}
}

119
src/app/financials/page.tsx Normal file
View File

@ -0,0 +1,119 @@
"use client";
import React from 'react';
import AppLayout from '@/components/AppLayout/AppLayout';
import {
DollarSign,
TrendingUp,
TrendingDown,
PieChart,
Calendar,
Filter,
Download,
ArrowUpRight,
ArrowDownRight,
ChevronRight,
LayoutDashboard,
Wallet
} from 'lucide-react';
import styles from './financials.module.css';
const sidebarItems = [
{ icon: <LayoutDashboard size={20} />, label: 'Dashboard' },
{ icon: <Wallet size={20} />, label: 'Financial Reports', active: true },
];
const metrics = [
{ label: 'Total Revenue', value: '$ 42,850', sub: '+18% vs last month', isUp: true, color: '#017e84' },
{ label: 'Ingredient Costs', value: '$ 12,400', sub: '+5% vs last month', isUp: false, color: '#ef4444' },
{ label: 'Net Profit', value: '$ 30,450', sub: '+24% vs last month', isUp: true, color: '#714b67' },
{ label: 'Avg. Check', value: '$ 64.20', sub: '-2% vs last week', isUp: false, color: '#f59e0b' },
];
const topItems = [
{ name: 'Classic Burger', sales: 450, revenue: '$ 5,845', margin: '65%' },
{ name: 'Margherita Pizza', sales: 320, revenue: '$ 4,640', margin: '72%' },
{ name: 'Caesar Salad', sales: 210, revenue: '$ 1,680', margin: '80%' },
];
export default function FinancialsPage() {
return (
<AppLayout title="Financial Reporting" sidebarItems={sidebarItems}>
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.titleArea}>
<h2>Profit & Loss Insights</h2>
<p>Real-time financial performance and cost analysis</p>
</div>
<div className={styles.headerActions}>
<button className={styles.dateSelector}><Calendar size={18} /> January 2026</button>
<button className={styles.exportBtn}><Download size={18} /> Export PDF</button>
</div>
</div>
{/* Metrics Grid */}
<div className={styles.metricsGrid}>
{metrics.map((m, idx) => (
<div key={idx} className={styles.metricCard}>
<span className={styles.metricLabel}>{m.label}</span>
<div className={styles.metricMain}>
<span className={styles.metricValue}>{m.value}</span>
<div className={`${styles.badge} ${m.isUp ? styles.up : styles.down}`}>
{m.isUp ? <ArrowUpRight size={14} /> : <ArrowDownRight size={14} />}
{m.sub.split(' ')[0]}
</div>
</div>
<div className={styles.miniChart}>
<div className={styles.progress} style={{ width: '70%', background: m.color }}></div>
</div>
</div>
))}
</div>
<div className={styles.contentRow}>
{/* Sales vs Cost Chart Placeholder */}
<div className={styles.chartCard}>
<div className={styles.sectionHeader}>
<h3>Revenue vs. Ingredient Cost</h3>
<div className={styles.legend}>
<span className={styles.revenueL}>Revenue</span>
<span className={styles.costL}>Cost</span>
</div>
</div>
<div className={styles.visualPlaceholder}>
<svg viewBox="0 0 500 200" className={styles.svgChart}>
<path d="M0 150 Q 100 130, 200 140 T 300 80 T 400 60 T 500 20" fill="none" stroke="#714b67" strokeWidth="4" />
<path d="M0 180 Q 100 170, 200 175 T 300 140 T 400 130 T 500 110" fill="none" stroke="#ef4444" strokeWidth="2" strokeDasharray="5,5" />
</svg>
<div className={styles.chartLabels}>
<span>W1</span><span>W2</span><span>W3</span><span>W4</span>
</div>
</div>
</div>
{/* Top Products Margin */}
<div className={styles.topItemsCard}>
<div className={styles.sectionHeader}>
<h3>High Margin Items</h3>
</div>
<div className={styles.itemsList}>
{topItems.map((item, idx) => (
<div key={idx} className={styles.itemRow}>
<div className={styles.itemInfo}>
<strong>{item.name}</strong>
<span>{item.sales} sold</span>
</div>
<div className={styles.itemStats}>
<span className={styles.itemRevenue}>{item.revenue}</span>
<span className={styles.itemMargin}>{item.margin} Margin</span>
</div>
</div>
))}
</div>
<button className={styles.viewDetailed}>Full Menu Analysis <ChevronRight size={16} /></button>
</div>
</div>
</div>
</AppLayout>
);
}

127
src/app/globals.css Normal file
View File

@ -0,0 +1,127 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700;800&display=swap');
:root {
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--font-heading: 'Outfit', sans-serif;
/* Premium Palette */
--pk-primary: #714b67;
--pk-primary-hover: #5d3d53;
--pk-secondary: #017e84;
--pk-accent: #e91e63;
--bg-main: #f0f2f5;
--bg-card: rgba(255, 255, 255, 0.8);
--bg-sidebar: #2c3e50;
--text-main: #1a1a1a;
--text-muted: #64748b;
--text-on-dark: #f8fafc;
--border-radius: 12px;
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
/* Glassmorphism */
--glass-bg: rgba(255, 255, 255, 0.7);
--glass-border: rgba(255, 255, 255, 0.3);
--glass-blur: blur(12px);
}
[data-theme='dark'] {
--bg-main: #0f172a;
--bg-card: rgba(30, 41, 59, 0.7);
--text-main: #f8fafc;
--text-muted: #94a3b8;
--glass-bg: rgba(15, 23, 42, 0.7);
--glass-border: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
body {
font-family: var(--font-sans);
background: var(--bg-main);
color: var(--text-main);
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
min-height: 100vh;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-heading);
font-weight: 700;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.animate-fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* Utility Classes */
.glass {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
border: 1px solid var(--glass-border);
}
.gradient-text {
background: linear-gradient(135deg, var(--pk-primary), var(--pk-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}

View File

@ -0,0 +1,230 @@
.inventoryContainer {
display: flex;
flex-direction: column;
gap: 2rem;
}
.statsRow {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.statCard {
background: white;
padding: 1.5rem;
border-radius: 16px;
border: 1px solid #e2e8f0;
display: flex;
align-items: center;
gap: 1rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.statIcon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.statInfo {
display: flex;
flex-direction: column;
}
.statLabel {
font-size: 0.85rem;
color: #64748b;
font-weight: 600;
}
.statValue {
font-size: 1.25rem;
font-weight: 800;
color: #1e293b;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.searchWrapper {
flex: 1;
position: relative;
max-width: 500px;
}
.searchIcon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
}
.searchWrapper input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.5rem;
background: white;
border: 1px solid #e2e8f0;
border-radius: 12px;
outline: none;
transition: var(--transition);
}
.searchWrapper input:focus {
border-color: var(--pk-primary);
box-shadow: 0 0 0 4px rgba(113, 75, 103, 0.1);
}
.toolActions {
display: flex;
gap: 0.75rem;
}
.filterBtn,
.addBtn {
padding: 0.75rem 1.25rem;
border-radius: 10px;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
transition: var(--transition);
}
.filterBtn {
background: white;
color: #475569;
border: 1px solid #e2e8f0;
}
.addBtn {
background: var(--pk-primary);
color: white;
border: none;
}
.stockList {
background: white;
border-radius: 20px;
border: 1px solid #e2e8f0;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
.listHeader {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr 1fr 0.5fr;
padding: 1rem 1.5rem;
background: #f8fafc;
color: #64748b;
font-weight: 700;
font-size: 0.85rem;
border-bottom: 1px solid #e2e8f0;
}
.listBody {
display: flex;
flex-direction: column;
}
.listItem {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr 1fr 0.5fr;
padding: 1.25rem 1.5rem;
align-items: center;
border-bottom: 1px solid #f1f5f9;
transition: var(--transition);
cursor: pointer;
}
.listItem:hover {
background: #f8fafc;
}
.itemName {
display: flex;
align-items: center;
gap: 1rem;
font-weight: 700;
color: #1e293b;
}
.itemIcon {
width: 32px;
height: 32px;
background: #f1f5f9;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: var(--pk-primary);
font-weight: 800;
font-size: 0.75rem;
}
.itemCategory {
color: #64748b;
font-size: 0.9rem;
}
.itemQty,
.itemMin {
font-weight: 600;
font-size: 0.9rem;
}
.statusBadge {
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 700;
}
.instock {
background: #dcfce7;
color: #15803d;
}
.lowstock {
background: #fef9c3;
color: #854d0e;
}
.outofstock {
background: #fee2e2;
color: #b91c1c;
}
.actions {
display: flex;
align-items: center;
gap: 1rem;
color: #94a3b8;
}
.actionBtn {
background: none;
border: none;
color: inherit;
cursor: pointer;
}
.arrow {
opacity: 0;
transition: var(--transition);
transform: translateX(-5px);
}
.listItem:hover .arrow {
opacity: 1;
transform: translateX(0);
}

116
src/app/inventory/page.tsx Normal file
View File

@ -0,0 +1,116 @@
"use client";
import React, { useState } from 'react';
import AppLayout from '@/components/AppLayout/AppLayout';
import {
Package,
AlertTriangle,
TrendingDown,
TrendingUp,
Plus,
Search,
Filter,
BarChart2,
RefreshCw,
MoreVertical,
ChevronRight
} from 'lucide-react';
import styles from './inventory.module.css';
const sidebarItems = [
{ icon: <Package size={20} />, label: 'Stock Levels', active: true },
{ icon: <TrendingUp size={20} />, label: 'Incoming Orders' },
{ icon: <BarChart2 size={20} />, label: 'Forecast' },
{ icon: <RefreshCw size={20} />, label: 'Valuation' },
];
const mockItems = [
{ id: 1, name: 'Burger Buns', category: 'Bread', quantity: 150, unit: 'pcs', minLevel: 50, status: 'In Stock' },
{ id: 2, name: 'Beef Patties', category: 'Meat', quantity: 24, unit: 'kg', minLevel: 30, status: 'Low Stock' },
{ id: 3, name: 'Cooking Oil', category: 'Pantry', quantity: 0, unit: 'L', minLevel: 10, status: 'Out of Stock' },
{ id: 4, name: 'Cheddar Cheese', category: 'Dairy', quantity: 45, unit: 'kg', minLevel: 20, status: 'In Stock' },
{ id: 5, name: 'Potatoes', category: 'Vegetables', quantity: 200, unit: 'kg', minLevel: 100, status: 'In Stock' },
{ id: 6, name: 'Ketchup', category: 'Pantry', quantity: 12, unit: 'L', minLevel: 15, status: 'Low Stock' },
];
export default function InventoryPage() {
const [search, setSearch] = useState('');
const stats = [
{ label: 'Total Valuation', value: '$ 12,450', icon: <TrendingUp size={18} />, color: '#714B67' },
{ label: 'Low Stock Items', value: '4 Assets', icon: <AlertTriangle size={18} />, color: '#f59e0b' },
{ label: 'Out of Stock', value: '2 Items', icon: <TrendingDown size={18} />, color: '#ef4444' },
];
return (
<AppLayout title="Inventory" sidebarItems={sidebarItems}>
<div className={styles.inventoryContainer}>
{/* Stats Row */}
<div className={styles.statsRow}>
{stats.map((stat, idx) => (
<div key={idx} className={styles.statCard}>
<div className={styles.statIcon} style={{ background: `${stat.color}15`, color: stat.color }}>
{stat.icon}
</div>
<div className={styles.statInfo}>
<span className={styles.statLabel}>{stat.label}</span>
<span className={styles.statValue}>{stat.value}</span>
</div>
</div>
))}
</div>
{/* Toolbar */}
<div className={styles.toolbar}>
<div className={styles.searchWrapper}>
<Search size={18} className={styles.searchIcon} />
<input
type="text"
placeholder="Search products or ingredients..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className={styles.toolActions}>
<button className={styles.filterBtn}><Filter size={18} /> Filters</button>
<button className={styles.addBtn}><Plus size={18} /> Add Stock</button>
</div>
</div>
{/* Inventory List */}
<div className={styles.stockList}>
<div className={styles.listHeader}>
<span>Item Name</span>
<span>Category</span>
<span>On Hand</span>
<span>Min. Level</span>
<span>Status</span>
<span>Action</span>
</div>
<div className={styles.listBody}>
{mockItems.map(item => (
<div key={item.id} className={styles.listItem}>
<div className={styles.itemName}>
<div className={styles.itemIcon}>{item.name[0]}</div>
{item.name}
</div>
<span className={styles.itemCategory}>{item.category}</span>
<span className={styles.itemQty}>{item.quantity} {item.unit}</span>
<span className={styles.itemMin}>{item.minLevel} {item.unit}</span>
<div className={styles.statusCell}>
<span className={`${styles.statusBadge} ${styles[item.status.toLowerCase().replace(/ /g, '')]}`}>
{item.status}
</span>
</div>
<div className={styles.actions}>
<button className={styles.actionBtn}><MoreVertical size={16} /></button>
<ChevronRight size={16} className={styles.arrow} />
</div>
</div>
))}
</div>
</div>
</div>
</AppLayout>
);
}

152
src/app/kitchen/page.tsx Normal file
View File

@ -0,0 +1,152 @@
"use client";
import React, { useState, useEffect } from 'react';
import { ChefHat, Clock, CheckCircle, Check, RotateCcw, Truck } from 'lucide-react';
interface OrderItem {
productId: number;
name: string;
qty: number;
modifiers: string[];
}
interface Order {
_id: string;
tableName: string;
items: OrderItem[];
status: 'KITCHEN' | 'READY' | 'SERVED';
createdAt: string;
elapsed?: string;
}
export default function KitchenPage() {
const [orders, setOrders] = useState<Order[]>([]);
const fetchOrders = async () => {
try {
const res = await fetch('http://localhost:5000/api/pos/orders/kitchen');
if (res.ok) {
const data = await res.json();
setOrders(data);
}
} catch (e) {
console.error("Fetch kitchen error", e);
}
};
useEffect(() => {
fetchOrders();
const interval = setInterval(fetchOrders, 10000); // Poll every 10s
return () => clearInterval(interval);
}, []);
const updateStatus = async (id: string, status: string) => {
try {
await fetch(`http://localhost:5000/api/pos/orders/${id}/status`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status })
});
fetchOrders();
} catch (error) {
console.error("Update status error", error);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'KITCHEN': return '#f59e0b';
case 'READY': return '#10b981';
case 'SERVED': return '#3b82f6';
default: return '#64748b';
}
};
return (
<div style={{ background: '#0f172a', minHeight: '100vh', color: 'white', fontFamily: 'Inter, sans-serif' }}>
<header style={{ padding: '1.5rem 2rem', borderBottom: '1px solid #1e293b', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<ChefHat size={32} color="#f59e0b" />
<div>
<h1 style={{ margin: 0, fontSize: '1.5rem', fontWeight: 800 }}>KITCHEN DISPLAY SYSTEM</h1>
<span style={{ color: '#94a3b8', fontSize: '0.9rem' }}>Live Orders</span>
</div>
</div>
<div style={{ display: 'flex', gap: '2rem' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<span style={{ fontSize: '1.5rem', fontWeight: 800, color: '#f59e0b' }}>{orders.filter(o => o.status === 'KITCHEN').length}</span>
<span style={{ fontSize: '0.7rem', color: '#94a3b8', textTransform: 'uppercase' }}>Pending</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<span style={{ fontSize: '1.5rem', fontWeight: 800, color: '#10b981' }}>{orders.filter(o => o.status === 'READY').length}</span>
<span style={{ fontSize: '0.7rem', color: '#94a3b8', textTransform: 'uppercase' }}>Ready</span>
</div>
</div>
</header>
<main style={{ padding: '2rem', display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '1.5rem' }}>
{orders.length === 0 && (
<div style={{ gridColumn: '1/-1', textAlign: 'center', padding: '4rem', color: '#475569' }}>
<ChefHat size={64} style={{ marginBottom: '1rem', opacity: 0.5 }} />
<h2>No Active Orders</h2>
<p>Waiting for new orders from POS...</p>
</div>
)}
{orders.map(order => (
<div key={order._id} style={{ background: '#1e293b', borderRadius: '16px', overflow: 'hidden', border: `2px solid ${getStatusColor(order.status)}` }}>
<div style={{ padding: '1rem', background: getStatusColor(order.status), color: 'white', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 800, fontSize: '1.1rem' }}>{order.tableName}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.8rem', fontWeight: 600, background: 'rgba(0,0,0,0.2)', padding: '0.2rem 0.6rem', borderRadius: '8px' }}>
<Clock size={14} />
{new Date(order.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
<div style={{ padding: '1rem' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.8rem', maxHeight: '300px', overflowY: 'auto' }}>
{order.items.map((item, idx) => (
<div key={idx} style={{ display: 'flex', justifyContent: 'space-between', borderBottom: '1px dashed #334155', paddingBottom: '0.5rem' }}>
<div style={{ display: 'flex', gap: '0.8rem' }}>
<span style={{ fontWeight: 800, color: '#f59e0b', fontSize: '1.1rem' }}>{item.qty}x</span>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={{ fontWeight: 600, fontSize: '1rem' }}>{item.name}</span>
{item.modifiers && item.modifiers.length > 0 && (
<span style={{ fontSize: '0.8rem', color: '#94a3b8' }}>{item.modifiers.join(', ')}</span>
)}
</div>
</div>
</div>
))}
</div>
</div>
<div style={{ padding: '1rem', borderTop: '1px solid #334155', display: 'flex', gap: '1rem' }}>
{order.status === 'KITCHEN' && (
<button
onClick={() => updateStatus(order._id, 'READY')}
style={{ flex: 1, padding: '0.8rem', background: '#10b981', border: 'none', borderRadius: '12px', color: 'white', fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem' }}
>
<CheckCircle size={18} /> MARK READY
</button>
)}
{order.status === 'READY' && (
<button
onClick={() => updateStatus(order._id, 'SERVED')}
style={{ flex: 1, padding: '0.8rem', background: '#3b82f6', border: 'none', borderRadius: '12px', color: 'white', fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem' }}
>
<Truck size={18} /> SERVE
</button>
)}
{order.status === 'SERVED' && (
<button
onClick={() => updateStatus(order._id, 'COMPLETED')}
style={{ flex: 1, padding: '0.8rem', background: '#64748b', border: 'none', borderRadius: '12px', color: 'white', fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem' }}
>
COMPLETE
</button>
)}
</div>
</div>
))}
</main>
</div>
);
}

23
src/app/layout.tsx Normal file
View File

@ -0,0 +1,23 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "Dine360 - Premium ERP",
description: "Dine360 - Premium ERP",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body suppressHydrationWarning>
<div className="layout-wrapper">
{children}
</div>
</body>
</html>
);
}

288
src/app/page.module.css Normal file
View File

@ -0,0 +1,288 @@
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
position: relative;
overflow: hidden;
background-color: var(--bg-main);
}
.bgGlow1 {
position: absolute;
top: -10%;
left: -10%;
width: 40%;
height: 40%;
background: radial-gradient(circle, rgba(113, 75, 103, 0.1) 0%, transparent 70%);
filter: blur(80px);
pointer-events: none;
}
.bgGlow2 {
position: absolute;
bottom: -10%;
right: -10%;
width: 40%;
height: 40%;
background: radial-gradient(circle, rgba(1, 126, 132, 0.1) 0%, transparent 70%);
filter: blur(80px);
pointer-events: none;
}
.header {
width: 100%;
max-width: 1200px;
margin: 0 auto 3rem auto;
display: flex;
align-items: center;
justify-content: space-between;
animation: fadeIn 0.8s ease-out;
}
.logoArea {
display: flex;
align-items: center;
gap: 0.75rem;
}
.logoIcon {
width: 42px;
height: 42px;
background: white;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow);
color: var(--pk-primary);
font-weight: 800;
font-size: 1.5rem;
}
.logoTitle {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-main);
}
.logoSubtitle {
font-weight: 400;
color: var(--text-muted);
}
.searchWrapper {
flex: 1;
max-width: 400px;
margin: 0 2rem;
position: relative;
}
.searchIcon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 18px;
height: 18px;
color: var(--text-muted);
}
.searchInput {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.5rem;
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
border: 1px solid var(--glass-border);
border-radius: 14px;
outline: none;
transition: var(--transition);
color: var(--text-main);
}
.searchInput:focus {
border-color: var(--pk-primary);
box-shadow: 0 0 0 4px rgba(113, 75, 103, 0.1);
}
.actions {
display: flex;
align-items: center;
gap: 1rem;
}
.iconBtn {
background: none;
border: none;
padding: 0.5rem;
color: var(--text-muted);
cursor: pointer;
border-radius: 50%;
transition: var(--transition);
position: relative;
}
.iconBtn:hover {
background: rgba(0, 0, 0, 0.05);
color: var(--text-main);
}
.badge {
position: absolute;
top: 6px;
right: 6px;
width: 8px;
height: 8px;
background: var(--pk-accent);
border-radius: 50%;
border: 2px solid var(--bg-main);
}
.userSection {
display: flex;
align-items: center;
gap: 0.75rem;
}
.userAvatar {
width: 38px;
height: 38px;
border-radius: 50%;
background: linear-gradient(135deg, var(--pk-primary), var(--pk-secondary));
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: 600;
border: 2px solid white;
box-shadow: var(--shadow);
cursor: pointer;
}
.logoutBtn {
background: none;
border: none;
padding: 0.5rem;
color: var(--text-muted);
cursor: pointer;
border-radius: 50%;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
}
.logoutBtn:hover {
background: rgba(231, 76, 60, 0.1);
color: #e74c3c;
}
.loginBtn {
padding: 0.6rem 1.2rem;
background: var(--pk-primary);
color: white;
border-radius: 10px;
text-decoration: none;
font-weight: 600;
font-size: 0.9rem;
transition: var(--transition);
box-shadow: var(--shadow);
}
.loginBtn:hover {
transform: translateY(-2px);
filter: brightness(1.1);
}
.mainGrid {
width: 100%;
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 2.5rem;
padding: 1rem;
}
.appItem {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
text-decoration: none;
color: inherit;
cursor: pointer;
transition: var(--transition);
}
.appIconWrapper {
width: 84px;
height: 84px;
border-radius: 22px;
display: flex;
align-items: center;
justify-content: center;
color: white;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.1);
}
.appItem:hover .appIconWrapper {
transform: translateY(-8px) scale(1.05) rotate(2deg);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.appName {
font-weight: 600;
color: var(--text-main);
font-size: 0.95rem;
text-align: center;
}
.footerActions {
position: fixed;
bottom: 2rem;
right: 2rem;
display: flex;
gap: 0.5rem;
padding: 0.5rem;
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
border: 1px solid var(--glass-border);
border-radius: 18px;
box-shadow: var(--shadow-lg);
}
.footerBtn {
background: none;
border: none;
padding: 0.75rem;
border-radius: 12px;
color: var(--text-muted);
cursor: pointer;
transition: var(--transition);
}
.footerBtn:hover {
background: rgba(0, 0, 0, 0.05);
color: var(--pk-primary);
}
@media (max-width: 768px) {
.header {
flex-direction: column;
gap: 1.5rem;
}
.searchWrapper {
max-width: 100%;
margin: 0;
}
.mainGrid {
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
}

184
src/app/page.tsx Normal file
View File

@ -0,0 +1,184 @@
"use client";
import React, { useState, useEffect } from 'react';
import {
BarChart3,
MessageSquare,
Users,
ShoppingCart,
Package,
Settings,
Search,
Grid,
Bell,
Calendar,
LayoutDashboard,
Wallet,
Building2,
Cpu,
Utensils,
LogOut
} from 'lucide-react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { deleteCookie, getCookie } from 'cookies-next';
import styles from './page.module.css';
// Restaurant-focused modules only
const apps = [
{ id: 'restaurant', name: 'Restaurant', icon: <Utensils size={32} />, color: '#F1C40F' },
{ id: 'pos', name: 'POS', icon: <ShoppingCart size={32} />, color: '#27AE60' },
{ id: 'inventory', name: 'Inventory', icon: <Package size={32} />, color: '#786FA6' },
{ id: 'accounting', name: 'Accounting', icon: <Wallet size={32} />, color: '#546DE5' },
{ id: 'calendar', name: 'Calendar', icon: <Calendar size={32} />, color: '#017E84' },
{ id: 'settings', name: 'Settings', icon: <Settings size={32} />, color: '#7F8C8D' },
];
export default function Home() {
const [search, setSearch] = useState('');
const [user, setUser] = useState<{ id: string; name: string; role: string; email: string } | null>(null);
const [loading, setLoading] = useState(true);
const router = useRouter();
useEffect(() => {
const checkAuth = async () => {
const token = getCookie('auth_token');
if (!token) {
setUser(null);
setLoading(false);
return;
}
try {
const res = await fetch('http://localhost:5000/api/auth/me', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await res.json();
if (res.ok) {
setUser(data);
} else {
deleteCookie('auth_token');
setUser(null);
}
} catch (err) {
setUser(null);
} finally {
setLoading(false);
}
};
checkAuth();
}, []);
const handleLogout = () => {
deleteCookie('auth_token');
localStorage.removeItem('user');
setUser(null);
router.push('/auth/login');
};
const filteredApps = apps.filter(app => {
const matchesSearch = app.name.toLowerCase().includes(search.toLowerCase());
if (!matchesSearch) return false;
if (loading) return true;
if (!user) return true; // Show all for guests (landing page feel)
if (user.role === 'admin') return true;
const rolePermissions: Record<string, string[]> = {
'waiter': ['POS', 'Restaurant', 'Calendar'],
'cashier': ['POS', 'Accounting', 'Calendar'],
'manager': ['Restaurant', 'POS', 'Inventory', 'Accounting', 'Calendar', 'Settings'],
};
const allowedApps = rolePermissions[user.role as keyof typeof rolePermissions] || [];
return allowedApps.includes(app.name);
});
return (
<div className={styles.container}>
<div className={styles.bgGlow1} />
<div className={styles.bgGlow2} />
<header className={styles.header}>
<div className={styles.logoArea}>
{/* <div className={styles.logoIcon}>O</div> */}
<h1 className={styles.logoTitle}>Dine360</h1>
</div>
<div className={styles.searchWrapper}>
<Search className={styles.searchIcon} />
<input
type="text"
placeholder="Search your apps..."
className={styles.searchInput}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className={styles.actions}>
<button className={styles.iconBtn}>
<Bell size={20} />
<div className={styles.badge} />
</button>
{user ? (
<div className={styles.userSection}>
<div className={styles.userAvatar} title={user.name}>
{user.name.substring(0, 2).toUpperCase()}
</div>
<button className={styles.logoutBtn} onClick={handleLogout} title="Logout">
<LogOut size={18} />
</button>
</div>
) : (
<Link href="/auth/login" className={styles.loginBtn}>Login</Link>
)}
</div>
</header>
<main className={styles.mainGrid}>
{filteredApps.map((app, index) => {
const content = (
<div
key={app.id}
className={styles.appItem}
style={{ animationDelay: `${index * 0.05}s` }}
>
<div
className={styles.appIconWrapper}
style={{
backgroundColor: app.color,
boxShadow: `0 12px 24px -8px ${app.color}60`
}}
>
{app.icon}
</div>
<span className={styles.appName}>{app.name}</span>
</div>
);
return (
<Link
key={app.id}
href={`/${app.id === 'employees' ? 'payroll' : app.id === 'discuss' ? 'dashboard' : app.id}`}
style={{ textDecoration: 'none' }}
>
{content}
</Link>
);
})}
</main>
<div className={styles.footerActions}>
<button className={styles.footerBtn} title="App Switcher">
<Grid size={24} />
</button>
<button className={styles.footerBtn} title="Home Dashboard">
<LayoutDashboard size={24} />
</button>
</div>
</div>
);
}

117
src/app/payroll/page.tsx Normal file
View File

@ -0,0 +1,117 @@
"use client";
import React, { useState } from 'react';
import AppLayout from '@/components/AppLayout/AppLayout';
import {
Users,
CreditCard,
Wallet,
Calendar,
FileCheck,
Plus,
Search,
TrendingUp,
Download,
AlertCircle,
LayoutDashboard,
Building2,
CheckCircle2
} from 'lucide-react';
import styles from './payroll.module.css';
const sidebarItems = [
{ icon: <LayoutDashboard size={20} />, label: 'Employee Dashboard' },
{ icon: <CreditCard size={20} />, label: 'Payslips', active: true },
{ icon: <Wallet size={20} />, label: 'Salary Structures' },
{ icon: <Calendar size={20} />, label: 'Attendances' },
];
const payslips = [
{ id: 'PSL/001', employee: 'Alice Johnson', period: 'January 2026', basic: '$ 3,500.00', allowance: '$ 450.00', deduction: '$ 200.00', net: '$ 3,750.00', status: 'Paid' },
{ id: 'PSL/002', employee: 'Bob Smith', period: 'January 2026', basic: '$ 2,800.00', allowance: '$ 200.00', deduction: '$ 150.00', net: '$ 2,850.00', status: 'Draft' },
{ id: 'PSL/003', employee: 'Charlie Dave', period: 'January 2026', basic: '$ 2,200.00', allowance: '$ 150.00', deduction: '$ 100.00', net: '$ 2,250.00', status: 'Waiting' },
{ id: 'PSL/004', employee: 'Diana Prince', period: 'January 2026', basic: '$ 4,200.00', allowance: '$ 600.00', deduction: '$ 350.00', net: '$ 4,450.00', status: 'Paid' },
];
export default function PayrollPage() {
const [search, setSearch] = useState('');
return (
<AppLayout title="Payroll Management" sidebarItems={sidebarItems}>
<div className={styles.container}>
{/* Payroll Summary */}
<div className={styles.summaryRow}>
<div className={styles.summaryCard}>
<div className={styles.cardInfo}>
<span className={styles.clabel}>Total Payroll (Jan)</span>
<span className={styles.cvalue}>$ 13,300.00</span>
</div>
<div className={styles.cardIcon} style={{ background: '#714b6715', color: '#714b67' }}><TrendingUp size={24} /></div>
</div>
<div className={styles.summaryCard}>
<div className={styles.cardInfo}>
<span className={styles.clabel}>Pending Payments</span>
<span className={styles.cvalue}>2 Batches</span>
</div>
<div className={styles.cardIcon} style={{ background: '#f59e0b15', color: '#f59e0b' }}><AlertCircle size={24} /></div>
</div>
<div className={styles.summaryCard}>
<div className={styles.cardInfo}>
<span className={styles.clabel}>Employees Paid</span>
<span className={styles.cvalue}>24 / 26</span>
</div>
<div className={styles.cardIcon} style={{ background: '#10b98115', color: '#10b981' }}><CheckCircle2 size={24} /></div>
</div>
</div>
{/* Toolbar */}
<div className={styles.toolbar}>
<div className={styles.searchWrapper}>
<Search size={18} className={styles.searchIcon} />
<input type="text" placeholder="Search employee payslips..." value={search} onChange={(e) => setSearch(e.target.value)} />
</div>
<div className={styles.actions}>
<button className={styles.batchBtn}><FileCheck size={18} /> Generate Batch</button>
<button className={styles.printBtn}><Download size={18} /> Export</button>
</div>
</div>
{/* Payslip Table */}
<div className={styles.tableWrapper}>
<table className={styles.payrollTable}>
<thead>
<tr>
<th>Reference</th>
<th>Employee</th>
<th>Period</th>
<th>Basic Salary</th>
<th>Allowances</th>
<th>Deductions</th>
<th>Net Wage</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{payslips.map((slip) => (
<tr key={slip.id} className={styles.row}>
<td className={styles.ref}>{slip.id}</td>
<td className={styles.employeeName}>{slip.employee}</td>
<td>{slip.period}</td>
<td>{slip.basic}</td>
<td style={{ color: '#10b981' }}>+ {slip.allowance}</td>
<td style={{ color: '#ef4444' }}>- {slip.deduction}</td>
<td className={styles.netWage}>{slip.net}</td>
<td>
<span className={`${styles.statusBadge} ${styles[slip.status.toLowerCase()]}`}>
{slip.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</AppLayout>
);
}

View File

@ -0,0 +1,185 @@
.container {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.summaryRow {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.summaryCard {
background: white;
padding: 1.5rem;
border-radius: 20px;
border: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.cardInfo {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.clabel {
font-size: 0.85rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
}
.cvalue {
font-size: 1.5rem;
font-weight: 800;
color: #1e293b;
font-family: var(--font-heading);
}
.cardIcon {
width: 50px;
height: 50px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.searchWrapper {
flex: 1;
position: relative;
max-width: 500px;
}
.searchIcon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
}
.searchWrapper input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.5rem;
background: white;
border: 1px solid #e2e8f0;
border-radius: 12px;
outline: none;
transition: var(--transition);
}
.actions {
display: flex;
gap: 0.75rem;
}
.batchBtn {
background: var(--pk-primary);
color: white;
border: none;
padding: 0.75rem 1.25rem;
border-radius: 12px;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.printBtn {
background: white;
border: 1px solid #e2e8f0;
padding: 0.75rem 1.25rem;
border-radius: 12px;
color: #64748b;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.tableWrapper {
background: white;
border-radius: 20px;
border: 1px solid #e2e8f0;
overflow: hidden;
box-shadow: var(--shadow);
}
.payrollTable {
width: 100%;
border-collapse: collapse;
text-align: left;
}
.payrollTable th {
padding: 1rem 1.5rem;
background: #f8fafc;
color: #64748b;
font-weight: 700;
font-size: 0.85rem;
border-bottom: 1px solid #e2e8f0;
}
.payrollTable td {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid #f1f5f9;
font-size: 0.9rem;
color: #1e293b;
}
.row:hover {
background: #f8fafc;
}
.ref {
font-weight: 700;
color: var(--pk-primary);
}
.employeeName {
font-weight: 700;
color: #1e293b;
}
.netWage {
font-weight: 800;
color: var(--pk-secondary);
font-family: var(--font-heading);
}
.statusBadge {
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 700;
}
.paid {
background: #dcfce7;
color: #15803d;
}
.draft {
background: #f1f5f9;
color: #64748b;
}
.waiting {
background: #fef9c3;
color: #854d0e;
}

View File

@ -0,0 +1,89 @@
import React, { useState, useEffect } from 'react';
import { PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
interface PaymentCheckoutProps {
amount: number;
onSuccess: () => void;
onCancel: () => void;
}
export default function PaymentCheckout({ amount, onSuccess, onCancel }: PaymentCheckoutProps) {
const stripe = useStripe();
const elements = useElements();
const [message, setMessage] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) {
return;
}
setIsLoading(true);
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
confirmParams: {
// Return URL is required for some payment methods, but we try to handle inline
return_url: window.location.origin + "/pos",
},
redirect: 'if_required',
});
if (error) {
setMessage(error.message || "An unexpected error occurred.");
setIsLoading(false);
} else if (paymentIntent && paymentIntent.status === 'succeeded') {
onSuccess();
// Do not setIsLoading(false) here to prevent flicker before modal closes
} else {
// Unexpected status
setMessage(`Payment Status: ${paymentIntent.status}`);
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit} style={{ width: '100%', padding: '1rem' }}>
<PaymentElement />
{message && <div style={{ color: '#ef4444', marginTop: '1rem', fontSize: '0.9rem', fontWeight: 600 }}>{message}</div>}
<div style={{ display: 'flex', gap: '1rem', marginTop: '2rem' }}>
<button
type="button"
onClick={onCancel}
style={{
flex: 1,
padding: '1rem',
background: 'transparent',
border: '2px solid #e2e8f0',
borderRadius: '16px',
color: '#64748b',
fontWeight: 700,
cursor: 'pointer'
}}
>
Cancel
</button>
<button
type="submit"
disabled={!stripe || isLoading}
style={{
flex: 2,
padding: '1rem',
background: '#0f172a',
color: 'white',
border: 'none',
borderRadius: '16px',
fontWeight: 800,
cursor: isLoading ? 'not-allowed' : 'pointer',
opacity: isLoading ? 0.7 : 1
}}
>
{isLoading ? "Processing..." : `Pay ₹${amount.toFixed(0)}`}
</button>
</div>
</form>
);
}

1198
src/app/pos/page.tsx Normal file

File diff suppressed because it is too large Load Diff

1296
src/app/pos/pos.module.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,253 @@
.menuContainer {
display: flex;
flex-direction: column;
gap: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.searchWrapper {
flex: 1;
position: relative;
max-width: 500px;
}
.searchIcon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
}
.searchWrapper input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.5rem;
background: white;
border: 1px solid #e2e8f0;
border-radius: 12px;
outline: none;
transition: var(--transition);
}
.actions {
display: flex;
gap: 1rem;
align-items: center;
}
.viewToggle {
display: flex;
background: #f1f5f9;
padding: 4px;
border-radius: 10px;
}
.viewToggle button {
padding: 0.5rem;
border: none;
background: transparent;
color: #64748b;
cursor: pointer;
border-radius: 8px;
transition: var(--transition);
}
.activeView {
background: white !important;
color: var(--pk-primary) !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.filterBtn,
.addBtn {
padding: 0.75rem 1.25rem;
border-radius: 10px;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.filterBtn {
background: white;
border: 1px solid #e2e8f0;
color: #475569;
}
.addBtn {
background: var(--pk-primary);
color: white;
border: none;
}
.menuGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 2rem;
}
.menuCard {
background: white;
border-radius: 20px;
border: 1px solid #e2e8f0;
overflow: hidden;
transition: var(--transition);
cursor: pointer;
}
.menuCard:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
}
.imagePlaceholder {
height: 160px;
background: #f8fafc;
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
}
.cardInfo {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.cardHeader {
display: flex;
justify-content: space-between;
align-items: center;
}
.category {
font-size: 0.75rem;
font-weight: 700;
color: var(--pk-secondary);
text-transform: uppercase;
}
.name {
font-size: 1.1rem;
font-weight: 800;
color: #1e293b;
}
.cardFooter {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.5rem;
}
.price {
font-weight: 800;
color: var(--pk-primary);
font-size: 1.25rem;
}
.editBtn {
background: #f1f5f9;
border: none;
padding: 0.5rem;
border-radius: 8px;
color: #64748b;
cursor: pointer;
}
.statusBadge {
padding: 0.2rem 0.6rem;
border-radius: 999px;
font-size: 0.7rem;
font-weight: 700;
}
.available {
background: #dcfce7;
color: #15803d;
}
.limited {
background: #fef9c3;
color: #854d0e;
}
.soldout {
background: #fee2e2;
color: #b91c1c;
}
.menuList {
background: white;
border-radius: 20px;
border: 1px solid #e2e8f0;
overflow: hidden;
}
.menuListItem {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
padding: 1rem 1.5rem;
align-items: center;
border-bottom: 1px solid #f1f5f9;
}
.itemMain {
display: flex;
align-items: center;
gap: 1rem;
}
.listIcon {
width: 40px;
height: 40px;
background: #f8fafc;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.itemDetail {
display: flex;
flex-direction: column;
}
.itemDetail strong {
color: #1e293b;
font-size: 0.95rem;
}
.itemDetail span {
color: #94a3b8;
font-size: 0.75rem;
}
.listPrice {
font-weight: 700;
color: var(--pk-primary);
}
.listActions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.listActions button {
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
padding: 4px;
}

View File

@ -0,0 +1,112 @@
"use client";
import React, { useState } from 'react';
import AppLayout from '@/components/AppLayout/AppLayout';
import {
Plus,
Search,
Image as ImageIcon,
Edit2,
Trash2,
ChevronRight,
Filter,
Grid,
List,
LayoutDashboard,
Utensils,
Monitor
} from 'lucide-react';
import styles from './menu.module.css';
const sidebarItems = [
{ icon: <LayoutDashboard size={20} />, label: 'Floor Plan' },
{ icon: <Utensils size={20} />, label: 'Orders' },
{ icon: <Monitor size={20} />, label: 'KDS (Kitchen)' },
{ icon: <Plus size={20} />, label: 'Menu Management', active: true },
];
const mockMenu = [
{ id: 1, name: 'Classic Burger', category: 'Main Course', price: 12.99, image: '🍔', status: 'Available' },
{ id: 2, name: 'Margherita Pizza', category: 'Main Course', price: 14.50, image: '🍕', status: 'Available' },
{ id: 3, name: 'Caesar Salad', category: 'Appetizers', price: 8.00, image: '🥗', status: 'Available' },
{ id: 4, name: 'French Fries', category: 'Sides', price: 4.50, image: '🍟', status: 'Available' },
{ id: 5, name: 'Chocolate Cake', category: 'Desserts', price: 6.50, image: '🍰', status: 'Limited' },
{ id: 6, name: 'Fresh Orange Juice', category: 'Beverages', price: 3.50, image: '🥤', status: 'Available' },
];
export default function MenuManagementPage() {
const [view, setView] = useState<'grid' | 'list'>('grid');
const [search, setSearch] = useState('');
return (
<AppLayout title="Restaurant Menu" sidebarItems={sidebarItems}>
<div className={styles.menuContainer}>
{/* Top Header */}
<div className={styles.header}>
<div className={styles.searchWrapper}>
<Search size={18} className={styles.searchIcon} />
<input
type="text"
placeholder="Search items..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className={styles.actions}>
<div className={styles.viewToggle}>
<button onClick={() => setView('grid')} className={view === 'grid' ? styles.activeView : ''}><Grid size={18} /></button>
<button onClick={() => setView('list')} className={view === 'list' ? styles.activeView : ''}><List size={18} /></button>
</div>
<button className={styles.filterBtn}><Filter size={18} /> Category</button>
<button className={styles.addBtn}><Plus size={18} /> New Item</button>
</div>
</div>
{/* Content */}
{view === 'grid' ? (
<div className={styles.menuGrid}>
{mockMenu.map(item => (
<div key={item.id} className={styles.menuCard}>
<div className={styles.imagePlaceholder}>{item.image}</div>
<div className={styles.cardInfo}>
<div className={styles.cardHeader}>
<span className={styles.category}>{item.category}</span>
<span className={`${styles.statusBadge} ${styles[item.status.toLowerCase()]}`}>{item.status}</span>
</div>
<h3 className={styles.name}>{item.name}</h3>
<div className={styles.cardFooter}>
<span className={styles.price}>$ {item.price.toFixed(2)}</span>
<div className={styles.cardActions}>
<button className={styles.editBtn}><Edit2 size={16} /></button>
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className={styles.menuList}>
{mockMenu.map(item => (
<div key={item.id} className={styles.menuListItem}>
<div className={styles.itemMain}>
<div className={styles.listIcon}>{item.image}</div>
<div className={styles.itemDetail}>
<strong>{item.name}</strong>
<span>{item.category}</span>
</div>
</div>
<span className={styles.listPrice}>$ {item.price.toFixed(2)}</span>
<span className={`${styles.statusBadge} ${styles[item.status.toLowerCase()]}`}>{item.status}</span>
<div className={styles.listActions}>
<button><Edit2 size={16} /></button>
<button><Trash2 size={16} /></button>
</div>
</div>
))}
</div>
)}
</div>
</AppLayout>
);
}

236
src/app/restaurant/page.tsx Normal file
View File

@ -0,0 +1,236 @@
"use client";
import React, { useState, useEffect } from 'react';
import AppLayout from '@/components/AppLayout/AppLayout';
import {
Users,
LayoutDashboard,
Utensils,
Plus,
Settings,
Grid,
Monitor,
CheckCircle2,
Clock,
ArrowRight,
Move,
Trash2,
Save,
Maximize2
} from 'lucide-react';
import { motion } from 'framer-motion';
import Link from 'next/link';
import styles from './restaurant.module.css';
const sidebarItems = [
{ icon: <LayoutDashboard size={20} />, label: 'Floor Plan', active: true },
{ icon: <Utensils size={20} />, label: 'Orders' },
{ icon: <Monitor size={20} />, label: 'KDS (Kitchen)' },
{ icon: <Plus size={20} />, label: 'Menu Management' },
];
const initialTables = [
{ id: 1, name: 'T1', seats: 2, status: 'available', x: 100, y: 100, shape: 'rect' as const },
{ id: 2, name: 'T2', seats: 4, status: 'occupied', x: 250, y: 100, shape: 'rect' as const, orderTotal: '$ 45.00' },
{ id: 3, name: 'T3', seats: 6, status: 'available', x: 450, y: 100, shape: 'circle' as const },
{ id: 4, name: 'T4', seats: 4, status: 'reserved', x: 100, y: 300, shape: 'rect' as const },
{ id: 5, name: 'T5', seats: 2, status: 'occupied', x: 300, y: 300, shape: 'circle' as const, orderTotal: '$ 12.50' },
];
export default function RestaurantPage() {
const [activeFloor, setActiveFloor] = useState('Main Floor');
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);
useEffect(() => {
fetch('/api/auth/me')
.then(res => res.json())
.then(data => {
if (data.user) {
setUser(data.user);
}
});
}, []);
const canEdit = user?.role === 'admin' || user?.role === 'manager';
const handleDrag = (id: number, info: any) => {
setTables(prev => prev.map(t =>
t.id === id ? { ...t, x: t.x + info.delta.x, y: t.y + info.delta.y } : t
));
};
const addTable = () => {
const newId = tables.length > 0 ? Math.max(...tables.map(t => t.id)) + 1 : 1;
setTables([...tables, {
id: newId,
name: `T${newId}`,
seats: 4,
status: 'available',
x: 50,
y: 50,
shape: 'rect'
}]);
setSelectedTable(newId);
};
const deleteTable = (id: number) => {
setTables(tables.filter(t => t.id !== id));
setSelectedTable(null);
};
const updateTable = (id: number, updates: any) => {
setTables(tables.map(t => t.id === id ? { ...t, ...updates } : t));
};
const currentTable = tables.find(t => t.id === selectedTable);
return (
<AppLayout title="Restaurant" sidebarItems={sidebarItems}>
<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
</button>
)}
</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>
</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>
</div>
)}
</motion.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>
</div>
<div className={styles.propGroup}>
<label>Identifier</label>
<input
type="text"
value={currentTable?.name}
onChange={(e) => updateTable(selectedTable, { name: e.target.value })}
/>
</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>
</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>
</AppLayout>
);
}

View File

@ -0,0 +1,424 @@
.restaurantContainer {
display: flex;
gap: 2rem;
height: calc(100vh - 160px);
}
.floorNav {
display: flex;
flex-direction: column;
gap: 0.75rem;
width: 200px;
}
.floorBtn {
padding: 0.75rem 1rem;
background: #ffffff;
border: 2px solid #e5e7eb;
border-radius: 14px;
text-align: left;
font-weight: 600;
color: #6b7280;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.floorBtn:hover {
background: #f9fafb;
color: #2dd4bf;
border-color: #2dd4bf;
transform: translateX(4px);
}
.activeFloor {
background: linear-gradient(135deg, #2dd4bf, #14b8a6) !important;
color: white !important;
border-color: #2dd4bf;
box-shadow: 0 4px 12px rgba(45, 212, 191, 0.3);
}
.addFloorBtn {
padding: 0.75rem;
background: #f9fafb;
border: 2px dashed #d1d5db;
border-radius: 14px;
color: #9ca3af;
cursor: pointer;
transition: all 0.3s ease;
}
.addFloorBtn:hover {
background: #ffffff;
border-color: #2dd4bf;
color: #2dd4bf;
}
.editToggleSection {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.editToggle {
padding: 0.75rem;
border-radius: 14px;
font-weight: 700;
border: 2px solid #e5e7eb;
background: #ffffff;
color: #374151;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.editToggle:hover {
border-color: #2dd4bf;
color: #2dd4bf;
}
.activeEdit {
background: linear-gradient(135deg, #14b8a6, #0d9488);
color: white;
border-color: #14b8a6;
box-shadow: 0 4px 12px rgba(20, 184, 166, 0.3);
}
.addTableBtn {
padding: 0.75rem;
background: linear-gradient(135deg, #2dd4bf, #14b8a6);
color: white;
border: none;
border-radius: 14px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(45, 212, 191, 0.3);
}
.addTableBtn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(45, 212, 191, 0.4);
}
.floorPlan {
flex: 1;
background: linear-gradient(135deg, #f0fdfa 0%, #ecfeff 100%);
border-radius: 24px;
position: relative;
overflow: hidden;
border: 2px solid #e5e7eb;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
}
.canvas {
width: 100%;
height: 100%;
background-image:
radial-gradient(circle, #d1d5db 1px, transparent 1px);
background-size: 30px 30px;
position: relative;
}
.legend {
position: absolute;
top: 1.5rem;
right: 1.5rem;
display: flex;
gap: 1.5rem;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 0.75rem 1.25rem;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.05);
z-index: 10;
}
.legendItem {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
font-weight: 600;
color: #475569;
}
.statusDot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.table {
width: 120px;
height: 120px;
cursor: pointer;
transition: all 0.3s ease;
}
.table:hover {
transform: scale(1.05);
}
.tableInnerLink {
text-decoration: none;
width: 100%;
height: 100%;
display: block;
}
.tableInner {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.25rem;
transition: all 0.3s ease;
position: relative;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.rect .tableInner {
border-radius: 16px;
}
.circle .tableInner {
border-radius: 50%;
}
.available .tableInner {
background: linear-gradient(135deg, #ffffff, #f0fdf4);
border: 3px solid #10b981;
box-shadow: 0 6px 0 #059669, 0 8px 20px rgba(16, 185, 129, 0.2);
}
.available:hover .tableInner {
box-shadow: 0 6px 0 #059669, 0 10px 25px rgba(16, 185, 129, 0.3);
}
.occupied .tableInner {
background: linear-gradient(135deg, #fee2e2, #fecaca);
border: 3px solid #ef4444;
color: #991b1b;
box-shadow: 0 6px 0 #dc2626, 0 8px 20px rgba(239, 68, 68, 0.2);
}
.occupied:hover .tableInner {
box-shadow: 0 6px 0 #dc2626, 0 10px 25px rgba(239, 68, 68, 0.3);
}
.reserved .tableInner {
background: linear-gradient(135deg, #fef3c7, #fde68a);
border: 3px solid #f59e0b;
color: #92400e;
box-shadow: 0 6px 0 #d97706, 0 8px 20px rgba(245, 158, 11, 0.2);
}
.reserved:hover .tableInner {
box-shadow: 0 6px 0 #d97706, 0 10px 25px rgba(245, 158, 11, 0.3);
}
.selectedTable .tableInner {
border-color: #2dd4bf !important;
box-shadow: 0 0 0 4px rgba(45, 212, 191, 0.3), 0 8px 20px rgba(45, 212, 191, 0.4) !important;
}
.dragHandle {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
background: var(--pk-primary);
color: white;
padding: 4px;
border-radius: 4px;
cursor: move;
}
.tableName {
font-weight: 800;
font-size: 1.25rem;
}
.seats {
font-size: 0.75rem;
opacity: 0.8;
font-weight: 600;
}
.orderTotal {
font-weight: 700;
font-size: 0.85rem;
margin-top: 4px;
}
.rightSidebar {
width: 300px;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.propertyEditor,
.kdsPreview {
background: #ffffff;
border-radius: 20px;
padding: 1.5rem;
border: 2px solid #e5e7eb;
display: flex;
flex-direction: column;
gap: 1.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.sidebarHeader {
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebarHeader h3 {
font-size: 1rem;
color: #111827;
font-weight: 800;
}
.propGroup {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.propGroup label {
font-size: 0.8rem;
font-weight: 700;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.propGroup input {
padding: 0.75rem;
border: 2px solid #e5e7eb;
border-radius: 12px;
outline: none;
transition: all 0.3s ease;
background: #f9fafb;
color: #111827;
font-weight: 600;
}
.propGroup input:focus {
border-color: #2dd4bf;
background: #ffffff;
box-shadow: 0 0 0 3px rgba(45, 212, 191, 0.1);
}
.shapeToggle {
display: flex;
gap: 0.5rem;
background: #f3f4f6;
padding: 4px;
border-radius: 12px;
}
.shapeToggle button {
flex: 1;
padding: 0.5rem;
border: none;
background: transparent;
border-radius: 10px;
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
color: #6b7280;
transition: all 0.3s ease;
}
.shapeToggle button:hover {
color: #2dd4bf;
}
.activeShape {
background: white !important;
color: #2dd4bf !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.deleteBtn {
background: #fee2e2;
color: #dc2626;
border: 2px solid #fecaca;
padding: 0.5rem;
border-radius: 10px;
cursor: pointer;
font-weight: 700;
transition: all 0.3s ease;
}
.deleteBtn:hover {
background: #fecaca;
border-color: #dc2626;
transform: translateY(-1px);
}
.kdsCount {
font-size: 0.75rem;
background: #dcfce7;
color: #15803d;
padding: 0.2rem 0.5rem;
border-radius: 6px;
font-weight: 700;
}
.kdsList {
display: flex;
flex-direction: column;
gap: 1rem;
}
.kdsItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: #f8fafc;
border-radius: 12px;
border: 1px solid #f1f5f9;
}
.kdsItemInfo strong {
display: block;
font-size: 0.9rem;
color: #1e293b;
}
.kdsItemInfo span {
font-size: 0.75rem;
color: #64748b;
}
.viewFullKds {
margin-top: auto;
text-align: center;
font-size: 0.85rem;
color: var(--pk-primary);
text-decoration: none;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}

View File

@ -0,0 +1,88 @@
"use client";
import React, { useState } from 'react';
import AppLayout from '@/components/AppLayout/AppLayout';
import {
Users,
UserPlus,
Map,
CheckCircle2,
XCircle,
ChevronRight,
MoreVertical,
LayoutDashboard,
Utensils,
Monitor,
Plus
} from 'lucide-react';
import styles from './waitstaff.module.css';
const sidebarItems = [
{ icon: <LayoutDashboard size={20} />, label: 'Floor Plan' },
{ icon: <Users size={20} />, label: 'Waitstaff Assignment', active: true },
{ icon: <Utensils size={20} />, label: 'Orders' },
{ icon: <Monitor size={20} />, label: 'KDS (Kitchen)' },
];
const staffList = [
{ id: 1, name: 'Alice Johnson', role: 'Senior Waiter', tables: ['T1', 'T2', 'T3'], status: 'active', avatar: 'AJ' },
{ id: 2, name: 'Bob Smith', role: 'Waiter', tables: ['T4', 'T5'], status: 'active', avatar: 'BS' },
{ id: 3, name: 'Charlie Dave', role: 'Junior Waiter', tables: ['T6'], status: 'on_break', avatar: 'CD' },
{ id: 4, name: 'Diana Prince', role: 'Senior Waiter', tables: [], status: 'inactive', avatar: 'DP' },
];
export default function WaitstaffPage() {
return (
<AppLayout title="Waitstaff Management" sidebarItems={sidebarItems}>
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.titleArea}>
<h2>Team Assignment</h2>
<p>Manage waitstaff table zones and current shifts</p>
</div>
<button className={styles.addBtn}><UserPlus size={18} /> Add Member</button>
</div>
<div className={styles.staffGrid}>
{staffList.map(staff => (
<div key={staff.id} className={styles.staffCard}>
<div className={styles.cardHeader}>
<div className={styles.avatar}>{staff.avatar}</div>
<div className={styles.statusBadge}>
{staff.status === 'active' ? <CheckCircle2 size={14} color="#10b981" /> : <XCircle size={14} color="#94a3b8" />}
<span className={styles[staff.status]}>{staff.status.replace('_', ' ')}</span>
</div>
</div>
<div className={styles.staffInfo}>
<h3 className={styles.staffName}>{staff.name}</h3>
<span className={styles.staffRole}>{staff.role}</span>
</div>
<div className={styles.assignmentSection}>
<div className={styles.sectionHeader}>
<Map size={14} /> Assigned Tables
</div>
<div className={styles.tableBadges}>
{staff.tables.length > 0 ? (
staff.tables.map(table => (
<span key={table} className={styles.tableBadge}>{table}</span>
))
) : (
<span className={styles.noTables}>No tables assigned</span>
)}
<button className={styles.assignMore}><Plus size={12} /></button>
</div>
</div>
<div className={styles.footer}>
<button className={styles.viewPerf}>View Performance</button>
<MoreVertical size={18} className={styles.moreIcon} />
</div>
</div>
))}
</div>
</div>
</AppLayout>
);
}

View File

@ -0,0 +1,206 @@
.container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.titleArea h2 {
font-size: 1.5rem;
font-weight: 800;
color: #1e293b;
}
.titleArea p {
color: #64748b;
font-size: 0.9rem;
}
.addBtn {
background: var(--pk-primary);
color: white;
border: none;
padding: 0.75rem 1.25rem;
border-radius: 12px;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
transition: var(--transition);
}
.staffGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 2rem;
}
.staffCard {
background: white;
border-radius: 24px;
border: 1px solid #e2e8f0;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
transition: var(--transition);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.staffCard:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
border-color: var(--pk-primary);
}
.cardHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.avatar {
width: 50px;
height: 50px;
background: #f1f5f9;
border: 2px solid white;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
color: var(--pk-primary);
box-shadow: var(--shadow);
}
.statusBadge {
display: flex;
align-items: center;
gap: 0.25rem;
background: #f8fafc;
padding: 4px 8px;
border-radius: 999px;
border: 1px solid #f1f5f9;
}
.statusBadge span {
font-size: 0.75rem;
font-weight: 700;
text-transform: capitalize;
}
.active {
color: #10b981;
}
.on_break {
color: #f59e0b;
}
.inactive {
color: #94a3b8;
}
.staffInfo {
display: flex;
flex-direction: column;
}
.staffName {
font-size: 1.1rem;
font-weight: 800;
color: #1e293b;
}
.staffRole {
font-size: 0.85rem;
color: #64748b;
font-weight: 500;
}
.assignmentSection {
background: #f8fafc;
padding: 1rem;
border-radius: 16px;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.sectionHeader {
font-size: 0.75rem;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 0.5rem;
}
.tableBadges {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.tableBadge {
background: white;
color: #1e293b;
font-weight: 700;
font-size: 0.75rem;
padding: 4px 10px;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.noTables {
font-size: 0.8rem;
color: #94a3b8;
font-style: italic;
}
.assignMore {
width: 26px;
height: 26px;
border-radius: 6px;
border: 1px dashed #cbd5e1;
background: transparent;
color: #94a3b8;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.viewPerf {
font-size: 0.85rem;
font-weight: 700;
color: var(--pk-primary);
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.viewPerf:hover {
text-decoration: underline;
}
.moreIcon {
color: #94a3b8;
cursor: pointer;
}

70
src/app/sales/page.tsx Normal file
View File

@ -0,0 +1,70 @@
"use client";
import React from 'react';
import AppLayout from '@/components/AppLayout/AppLayout';
import {
BarChart3,
Users,
ShoppingCart,
Package,
LayoutDashboard,
FileText,
CreditCard,
Truck
} from 'lucide-react';
import styles from './sales.module.css';
const sidebarItems = [
{ icon: <LayoutDashboard size={20} />, label: 'Dashboard' },
{ icon: <FileText size={20} />, label: 'Quotations', active: true },
{ icon: <ShoppingCart size={20} />, label: 'Orders' },
{ icon: <Users size={20} />, label: 'Customers' },
{ icon: <Package size={20} />, label: 'Products' },
];
const mockOrders = [
{ id: 'S0001', customer: 'Azure Interior', date: '2026-01-20', salesman: 'James Doe', total: '$ 1,250.00', status: 'Quotation' },
{ id: 'S0002', customer: 'Deco Addict', date: '2026-01-21', salesman: 'Alice Smith', total: '$ 4,500.00', status: 'Sales Order' },
{ id: 'S0003', customer: 'Ready Mat', date: '2026-01-21', salesman: 'James Doe', total: '$ 800.00', status: 'Sent' },
{ id: 'S0004', customer: 'Gemini Corp', date: '2026-01-22', salesman: 'James Doe', total: '$ 12,000.00', status: 'Sales Order' },
{ id: 'S0005', customer: 'The Corner', date: '2026-01-22', salesman: 'Bob Wilson', total: '$ 320.00', status: 'Cancelled' },
];
export default function SalesPage() {
return (
<AppLayout title="Sales" sidebarItems={sidebarItems}>
<div className={styles.tableContainer}>
<table className={styles.table}>
<thead>
<tr>
<th><input type="checkbox" /></th>
<th>Number</th>
<th>Customer</th>
<th>Order Date</th>
<th>Salesperson</th>
<th>Total</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{mockOrders.map((order) => (
<tr key={order.id} className={styles.row}>
<td><input type="checkbox" /></td>
<td className={styles.orderId}>{order.id}</td>
<td>{order.customer}</td>
<td>{order.date}</td>
<td>{order.salesman}</td>
<td className={styles.total}>{order.total}</td>
<td>
<span className={`${styles.status} ${styles[order.status.replace(' ', '').toLowerCase()]}`}>
{order.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</AppLayout>
);
}

View File

@ -0,0 +1,70 @@
.tableContainer {
background: white;
border-radius: 12px;
border: 1px solid #e2e8f0;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.table {
width: 100%;
border-collapse: collapse;
text-align: left;
}
.table th {
padding: 1rem;
background: #f8fafc;
color: #64748b;
font-weight: 600;
font-size: 0.85rem;
border-bottom: 1px solid #e2e8f0;
}
.table td {
padding: 1rem;
border-bottom: 1px solid #f1f5f9;
font-size: 0.9rem;
color: #1e293b;
}
.row:hover {
background: #f8fafc;
cursor: pointer;
}
.orderId {
font-weight: 600;
color: var(--pk-primary);
}
.total {
font-weight: 600;
}
.status {
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.quotation {
background: #e0f2fe;
color: #0369a1;
}
.salesorder {
background: #dcfce7;
color: #15803d;
}
.sent {
background: #fef9c3;
color: #854d0e;
}
.cancelled {
background: #fee2e2;
color: #b91c1c;
}

View File

@ -0,0 +1,243 @@
.appContainer {
display: flex;
height: 100vh;
background-color: #f8fafc;
}
.sidebar {
width: 64px;
background-color: var(--bg-sidebar);
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem 0;
gap: 1.5rem;
z-index: 50;
}
.appSwitcher {
width: 44px;
height: 44px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
}
.appSwitcher:hover {
background: rgba(255, 255, 255, 0.2);
}
.sidebarMenu {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
}
.sidebarItem {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
color: #94a3b8;
cursor: pointer;
transition: var(--transition);
}
.sidebarItem:hover {
background: rgba(255, 255, 255, 0.05);
color: white;
}
.active {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.contentWrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.navbar {
height: 56px;
background: white;
border-bottom: 1px solid #e2e8f0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1.5rem;
}
.navLeft {
display: flex;
align-items: baseline;
gap: 1rem;
}
.appTitle {
font-weight: 700;
font-size: 1.1rem;
color: var(--pk-primary);
}
.breadcrumbs {
font-size: 0.9rem;
color: var(--text-muted);
}
.current {
color: var(--text-main);
font-weight: 500;
}
.navCenter {
flex: 1;
max-width: 500px;
}
.searchBar {
position: relative;
width: 100%;
}
.searchIcon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
}
.searchInput {
width: 100%;
padding: 0.5rem 1rem 0.5rem 2.5rem;
background: #f1f5f9;
border: 1px solid transparent;
border-radius: 8px;
outline: none;
transition: var(--transition);
}
.searchInput:focus {
background: white;
border-color: var(--pk-primary);
box-shadow: 0 0 0 3px rgba(113, 75, 103, 0.1);
}
.navRight {
display: flex;
align-items: center;
gap: 1rem;
}
.navBtn {
background: none;
border: none;
color: #64748b;
cursor: pointer;
padding: 0.5rem;
border-radius: 50%;
transition: var(--transition);
}
.navBtn:hover {
background: #f1f5f9;
}
.avatar {
width: 32px;
height: 32px;
background: var(--pk-primary);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
}
.actionBar {
height: 56px;
background: white;
border-bottom: 1px solid #e2e8f0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1.5rem;
}
.actionLeft {
display: flex;
gap: 0.75rem;
}
.primaryBtn {
background: var(--pk-primary);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
transition: var(--transition);
}
.primaryBtn:hover {
background: var(--pk-primary-hover);
}
.secondaryBtn {
background: white;
border: 1px solid #d1d5db;
padding: 0.5rem 1rem;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
.secondaryBtn:hover {
background: #f9fafb;
}
.viewToggle {
display: flex;
background: #f1f5f9;
padding: 3px;
border-radius: 8px;
}
.viewBtn {
background: none;
border: none;
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
color: #64748b;
}
.viewBtn:first-child {
background: white;
color: var(--text-main);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.mainContent {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
}

View File

@ -0,0 +1,135 @@
"use client";
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import {
Grid,
Settings,
Search,
Bell,
Plus,
Filter,
ArrowUpDown,
MoreVertical
} from 'lucide-react';
import styles from './AppLayout.module.css';
interface AppLayoutProps {
children: React.ReactNode;
title: string;
sidebarItems: { icon: React.ReactNode; label: string; active?: boolean }[];
}
export default function AppLayout({ children, title, sidebarItems }: AppLayoutProps) {
const [user, setUser] = useState<{ name: string; role: string } | null>(null);
const router = useRouter();
useEffect(() => {
fetch('/api/auth/me')
.then(res => res.json())
.then(data => {
if (data.user) {
setUser(data.user);
} else {
router.push('/auth/login');
}
})
.catch(() => router.push('/auth/login'));
}, [router]);
const filteredSidebarItems = sidebarItems.filter(item => {
if (!user) return false;
if (user.role === 'admin') return true;
// Define restrictions
const restrictions: Record<string, string[]> = {
'waiter': ['Floor Plan', 'Orders', 'KDS (Kitchen)'],
'cashier': ['Orders', 'POS Dashboard'],
'manager': ['Floor Plan', 'Orders', 'KDS (Kitchen)', 'Menu Management', 'Waitstaff Assignment'],
};
const allowedItems = restrictions[user.role as keyof typeof restrictions] || [];
return allowedItems.includes(item.label);
});
return (
<div className={styles.appContainer}>
{/* Sidebar */}
<aside className={styles.sidebar}>
<Link href="/" className={styles.appSwitcher}>
<Grid size={24} color="white" />
</Link>
<div className={styles.sidebarMenu}>
{filteredSidebarItems.map((item: any, idx: number) => {
let href = '#';
if (item.label === 'Floor Plan') href = '/restaurant';
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 === 'Employee Dashboard') href = '/payroll';
return (
<Link key={idx} href={href} style={{ textDecoration: 'none' }}>
<div className={`${styles.sidebarItem} ${item.active ? styles.active : ''}`} title={item.label}>
{item.icon}
</div>
</Link>
);
})}
</div>
<div className={styles.sidebarBottom}>
<Settings size={20} color="#94a3b8" />
</div>
</aside>
{/* Main Content Area */}
<div className={styles.contentWrapper}>
{/* Navbar */}
<nav className={styles.navbar}>
<div className={styles.navLeft}>
<span className={styles.appTitle}>{title}</span>
<div className={styles.breadcrumbs}>
<span>Orders</span> / <span className={styles.current}>Sales Orders</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} />
</div>
</div>
<div className={styles.navRight}>
<button className={styles.navBtn}><Bell size={20} /></button>
<div className={styles.avatar}>{user?.name?.substring(0, 2).toUpperCase() || 'AR'}</div>
</div>
</nav>
{/* Action Bar */}
<div className={styles.actionBar}>
<div className={styles.actionLeft}>
<button className={styles.primaryBtn}><Plus size={18} /> New</button>
<button className={styles.secondaryBtn}>Import</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>
</div>
</div>
</div>
{/* Main Content */}
<main className={styles.mainContent}>
{children}
</main>
</div>
</div>
);
}

40
src/lib/mongodb.ts Normal file
View File

@ -0,0 +1,40 @@
import mongoose from 'mongoose';
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/odoo_next';
if (!MONGODB_URI) {
throw new Error('Please define the MONGODB_URI environment variable inside .env.local');
}
let cached = (global as any).mongoose;
if (!cached) {
cached = (global as any).mongoose = { conn: null, promise: null };
}
async function dbConnect() {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
bufferCommands: false,
};
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
return mongoose;
});
}
try {
cached.conn = await cached.promise;
} catch (e) {
cached.promise = null;
throw e;
}
return cached.conn;
}
export default dbConnect;

56
src/models/Restaurant.ts Normal file
View File

@ -0,0 +1,56 @@
import mongoose from 'mongoose';
// --- Category ---
const CategorySchema = new mongoose.Schema({
name: { type: String, required: true },
icon: String,
color: String,
});
export const Category = mongoose.models.Category || mongoose.model('Category', CategorySchema);
// --- Product ---
const ProductSchema = new mongoose.Schema({
name: { type: String, required: true },
price: { type: Number, required: true },
category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category' },
image: String,
available: { type: Boolean, default: true },
isAddon: { type: Boolean, default: false },
});
export const Product = mongoose.models.Product || mongoose.model('Product', ProductSchema);
// --- Table ---
const TableSchema = new mongoose.Schema({
name: { type: String, required: true },
seats: { type: Number, default: 4 },
status: { type: String, enum: ['available', 'occupied', 'reserved'], default: 'available' },
x: Number,
y: Number,
width: Number,
height: Number,
shape: { type: String, enum: ['rect', 'circle'], default: 'rect' },
currentOrderId: { type: mongoose.Schema.Types.ObjectId, ref: 'Order' },
});
export const Table = mongoose.models.Table || mongoose.model('Table', TableSchema);
// --- Order ---
const OrderSchema = new mongoose.Schema({
tableId: { type: mongoose.Schema.Types.ObjectId, ref: 'Table' },
waiterId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
items: [{
productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' },
quantity: { type: Number, default: 1 },
price: Number, // snapshot price
note: String,
}],
status: { type: String, enum: ['draft', 'sent', 'paid', 'cancelled'], default: 'draft' },
subtotal: Number,
tax: Number,
total: Number,
paymentMethod: String,
}, { timestamps: true });
export const Order = mongoose.models.Order || mongoose.model('Order', OrderSchema);

13
src/models/User.ts Normal file
View File

@ -0,0 +1,13 @@
import mongoose from 'mongoose';
const UserSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['admin', 'manager', 'waiter', 'cashier'], default: 'admin' },
avatar: String,
resetPasswordToken: String,
resetPasswordExpires: Date,
}, { timestamps: true });
export default mongoose.models.User || mongoose.model('User', UserSchema);

34
tsconfig.json Normal file
View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}