Added counter-frontend module to the repository

This commit is contained in:
2026-04-16 10:08:37 +05:30
parent 54172f6dff
commit 173864176f
19 changed files with 5376 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Counter POS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3576
counter-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
{
"name": "counter-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.38.0",
"lucide-react": "^1.7.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.14.0",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.4.27",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"postcss": "^8.5.8",
"tailwindcss": "^4.2.2",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.0",
"vite": "^8.0.4"
}
}

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Login from './pages/Login';
import POS from './pages/POS';
import Orders from './pages/Orders';
import Categories from './pages/Categories';
import Products from './pages/Products';
import Layout from './components/Layout';
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const isLoggedIn = sessionStorage.getItem('isCounterLoggedIn') === 'true';
return isLoggedIn ? <>{children}</> : <Navigate to="/login" replace />;
};
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={
<ProtectedRoute>
<Layout />
</ProtectedRoute>
}>
<Route index element={<Navigate to="/pos" replace />} />
<Route path="pos" element={<POS />} />
<Route path="orders" element={<Orders />} />
<Route path="categories" element={<Categories />} />
<Route path="products" element={<Products />} />
</Route>
</Routes>
</Router>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -0,0 +1,121 @@
import React from 'react';
import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom';
import {
LayoutDashboard,
ShoppingBag,
History,
Layers,
Package,
LogOut,
Menu,
Bell,
Search,
User
} from 'lucide-react';
import CollegeLogo from '../assets/college-logo.png';
const Layout = () => {
const location = useLocation();
const navigate = useNavigate();
const userName = sessionStorage.getItem('counterUserName') || 'Counter User';
const handleLogout = () => {
sessionStorage.removeItem('isCounterLoggedIn');
navigate('/login');
};
const navItems = [
{ path: '/pos', icon: ShoppingBag, label: 'POS' },
{ path: '/orders', icon: History, label: 'Orders' },
{ path: '/categories', icon: Layers, label: 'Categories' },
{ path: '/products', icon: Package, label: 'Products' },
];
return (
<div className="min-h-screen bg-[#f8fafc] flex font-inter overflow-hidden h-screen">
{/* Side Navigation Bar */}
<aside className="w-64 bg-[#231651] text-white flex flex-col shadow-2xl z-50">
<div className="p-8">
<div className="flex flex-col gap-6">
<div className="bg-white rounded-2xl p-4 flex items-center justify-center border border-white/20 shadow-xl overflow-hidden">
<img src={CollegeLogo} alt="Logo" className="h-12 w-auto object-contain" />
</div>
<div>
<span className="text-xl font-black tracking-tighter text-white">RIT COUNTER</span>
<p className="text-[10px] font-bold text-white/40 uppercase tracking-widest mt-1">Institutional POS</p>
</div>
</div>
</div>
<nav className="flex-1 px-4 space-y-2">
{navItems.map((item) => {
const isActive = location.pathname === item.path;
return (
<Link
key={item.path}
to={item.path}
className={`flex items-center gap-4 px-6 py-4 rounded-2xl text-sm font-bold transition-all ${
isActive
? 'bg-white/10 text-white shadow-lg'
: 'text-white/50 hover:text-white hover:bg-white/5'
}`}
>
<item.icon size={20} className={isActive ? 'text-white' : 'text-white/50'} />
{item.label}
</Link>
);
})}
</nav>
<div className="p-6">
<div className="bg-white/5 rounded-[2rem] p-6 border border-white/10 group cursor-pointer relative">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-white/10 rounded-xl flex items-center justify-center text-white border border-white/10">
<User size={20} />
</div>
<div className="flex-1 overflow-hidden">
<p className="text-xs font-black truncate">{userName}</p>
<p className="text-[10px] font-bold text-white/40 uppercase tracking-widest">Operator</p>
</div>
</div>
<button
onClick={handleLogout}
className="mt-4 w-full flex items-center justify-center gap-2 py-3 bg-rose-500/20 text-rose-400 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-rose-500 hover:text-white transition-all"
>
<LogOut size={14} /> Logout
</button>
</div>
</div>
</aside>
{/* Main Content Area */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Top Header for Mobile/Search/Notifications */}
<header className="h-20 bg-white border-b border-slate-100 flex items-center justify-between px-8 shrink-0">
<div className="flex items-center bg-slate-50 border border-slate-100 rounded-2xl px-6 py-2.5 gap-4 w-full max-w-xl focus-within:bg-white focus-within:ring-4 focus-within:ring-primary/5 transition-all">
<Search size={18} className="text-slate-400" />
<input
type="text"
placeholder="Search anything here..."
className="bg-transparent border-none outline-none text-sm font-medium w-full"
/>
</div>
<div className="flex items-center gap-6">
<button className="relative w-12 h-12 flex items-center justify-center bg-slate-50 rounded-2xl text-slate-400 hover:text-primary hover:bg-primary/5 transition-all">
<Bell size={20} />
<span className="absolute top-3 right-3 w-2 h-2 bg-rose-500 rounded-full border-2 border-white"></span>
</button>
</div>
</header>
<main className="flex-1 overflow-hidden relative">
<Outlet />
</main>
</div>
</div>
);
};
export default Layout;

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
interface PaginationProps {
currentPage: number;
pageSize: number;
totalElements: number;
onPageChange: (page: number) => void;
onPageSizeChange: (size: number) => void;
}
const Pagination: React.FC<PaginationProps> = ({
currentPage,
pageSize,
totalElements,
onPageChange,
onPageSizeChange
}) => {
const totalPages = Math.ceil(totalElements / pageSize);
const startElement = currentPage * pageSize + 1;
const endElement = Math.min((currentPage + 1) * pageSize, totalElements);
if (totalElements === 0) return null;
return (
<div className="px-8 py-6 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-6 bg-white shrink-0">
<div className="flex items-center gap-4">
<span className="text-xs font-black text-slate-400 uppercase tracking-widest leading-none">Show</span>
<select
value={pageSize}
onChange={(e) => onPageSizeChange(Number(e.target.value))}
className="bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 text-xs font-black text-slate-900 outline-none focus:ring-4 focus:ring-primary/5 focus:border-primary transition-all appearance-none pr-8 relative"
style={{ backgroundImage: 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' fill=\'none\' viewBox=\'0 0 24 24\' stroke=\'%2394a3b8\'%3E%3Cpath stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'M19 9l-7 7-7-7\'%3E%3C/path%3E%3C/svg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 0.5rem center', backgroundSize: '1rem' }}
>
{[5, 10, 25, 50].map(size => (
<option key={size} value={size}>{size} items</option>
))}
</select>
<span className="text-[10px] font-bold text-slate-300 uppercase tracking-widest whitespace-nowrap">
{startElement}-{endElement} of {totalElements} units
</span>
</div>
<div className="flex items-center gap-2">
<button
disabled={currentPage === 0}
onClick={() => onPageChange(currentPage - 1)}
className="w-10 h-10 flex items-center justify-center rounded-xl border border-slate-100 text-slate-400 hover:text-primary hover:bg-primary/5 disabled:opacity-30 disabled:hover:bg-transparent transition-all"
>
<ChevronLeft size={18} />
</button>
<div className="flex items-center gap-1">
{Array.from({ length: totalPages }, (_, i) => (
<button
key={i}
onClick={() => onPageChange(i)}
className={`w-10 h-10 font-bold text-xs rounded-xl transition-all ${
currentPage === i
? 'bg-primary text-white shadow-lg shadow-primary/20'
: 'text-slate-400 hover:bg-slate-50'
}`}
>
{i + 1}
</button>
)).slice(Math.max(0, currentPage - 1), Math.min(totalPages, currentPage + 2))}
</div>
<button
disabled={currentPage >= totalPages - 1}
onClick={() => onPageChange(currentPage + 1)}
className="w-10 h-10 flex items-center justify-center rounded-xl border border-slate-100 text-slate-400 hover:text-primary hover:bg-primary/5 disabled:opacity-30 disabled:hover:bg-transparent transition-all"
>
<ChevronRight size={18} />
</button>
</div>
</div>
);
};
export default Pagination;

View File

@@ -0,0 +1,8 @@
@import "tailwindcss";
@theme {
--color-primary: #231651;
--color-secondary: #64748b;
--color-background: #f8fafc;
--font-inter: "Inter", sans-serif;
}

View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,256 @@
import React, { useState, useEffect } from 'react';
import {
Layers,
Package,
Search,
Plus,
Edit2,
Trash2,
X,
Check
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
const Categories = () => {
const [categories, setCategories] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [currentCategory, setCurrentCategory] = useState<any>(null);
const fetchCategories = async () => {
setLoading(true);
try {
const host = window.location.hostname;
const response = await fetch(`http://${host}:8080/api/base-items`);
const data = await response.json();
setCategories(Array.isArray(data) ? data : (data.content || []));
} catch (error) {
console.error('Error fetching categories:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchCategories();
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const host = window.location.hostname;
const url = currentCategory?.id
? `http://${host}:8080/api/base-items/${currentCategory.id}`
: `http://${host}:8080/api/base-items`;
const method = currentCategory?.id ? 'PUT' : 'POST';
try {
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(currentCategory)
});
if (response.ok) {
setIsModalOpen(false);
fetchCategories();
}
} catch (error) {
console.error('Error saving category:', error);
}
};
const filteredCategories = categories.filter(cat =>
cat.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="p-8 space-y-8 h-full overflow-y-auto custom-scrollbar bg-slate-50">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-4xl font-black text-slate-900 tracking-tight">Food Categories</h1>
<p className="text-slate-400 text-xs font-bold uppercase tracking-[0.2em] mt-2">Manage your menu structure</p>
</div>
<button
onClick={() => {
setCurrentCategory({ name: '', description: '', active: true });
setIsModalOpen(true);
}}
className="bg-primary text-white px-10 py-4 rounded-2xl font-black text-xs uppercase tracking-widest flex items-center gap-3 shadow-2xl shadow-primary/30 hover:scale-[1.02] active:scale-95 transition-all"
>
<Plus size={18} /> Add Category
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<StatCard icon={Layers} label="Active Categories" value={categories.filter(c => c.active).length} color="bg-primary/5 text-primary" />
<StatCard icon={Package} label="Total Structure" value={categories.length} color="bg-emerald-50 text-emerald-500" />
</div>
<div className="bg-white p-4 rounded-[3rem] shadow-xl shadow-slate-200/50 border border-slate-100 overflow-hidden">
<div className="p-6 relative group">
<Search className="absolute left-12 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-primary transition-colors" size={24} />
<input
type="text"
placeholder="Search categories by name..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-slate-50 border border-slate-100/50 rounded-2xl py-6 pl-16 pr-8 text-sm font-semibold outline-none focus:bg-white focus:ring-4 focus:ring-primary/5 focus:border-primary transition-all shadow-inner"
/>
</div>
<div className="p-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
<AnimatePresence>
{loading ? (
<div className="col-span-full h-40 flex items-center justify-center">
<div className="w-10 h-10 border-4 border-primary/20 border-t-primary rounded-full animate-spin"></div>
</div>
) : filteredCategories.map((cat) => (
<motion.div
layout
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
key={cat.id}
className="bg-white p-8 rounded-[2.8rem] border border-slate-100 shadow-sm hover:shadow-2xl transition-all group flex flex-col gap-8 relative overflow-hidden"
>
<div className={`absolute top-0 right-0 w-32 h-32 -mr-16 -mt-16 rounded-full opacity-[0.03] transition-transform duration-700 group-hover:scale-150 ${cat.active ? 'bg-emerald-500' : 'bg-slate-500'}`} />
<div className="flex items-center justify-between relative z-10">
<div className={`w-16 h-16 rounded-2xl flex items-center justify-center transition-all ${cat.active ? 'bg-primary/5 text-primary' : 'bg-slate-100 text-slate-400'} group-hover:bg-primary group-hover:text-white`}>
<Layers size={32} />
</div>
<div className="flex gap-2 opacity-0 group-hover:opacity-100 transition-all transform translate-x-4 group-hover:translate-x-0">
<button
onClick={() => window.location.href = `/products?category=${cat.name}`}
className="w-10 h-10 bg-white border border-slate-100 rounded-xl flex items-center justify-center text-slate-400 hover:text-emerald-500 hover:border-emerald-500/30 shadow-sm transition-all"
title="View Products"
>
<Package size={16} />
</button>
<button
onClick={() => {
setCurrentCategory(cat);
setIsModalOpen(true);
}}
className="w-10 h-10 bg-white border border-slate-100 rounded-xl flex items-center justify-center text-slate-400 hover:text-primary hover:border-primary/30 shadow-sm transition-all"
title="Edit Category"
>
<Edit2 size={16} />
</button>
</div>
</div>
<div className="relative z-10">
<h3 className="text-xl font-black text-slate-800 uppercase tracking-tight leading-none mb-3">{cat.name}</h3>
<div className="flex items-center gap-3">
<span className={`w-2 h-2 rounded-full ${cat.active ? 'bg-emerald-500 animate-pulse' : 'bg-slate-300'}`} />
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">{cat.active ? 'Active' : 'Inactive'}</p>
</div>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
{/* CRUD Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
onClick={() => setIsModalOpen(false)}
className="absolute inset-0 bg-slate-900/60 backdrop-blur-md"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
className="relative bg-white w-full max-w-lg rounded-[3rem] shadow-2xl overflow-hidden"
>
<div className="p-10 border-b border-slate-100 flex items-center justify-between bg-white">
<div>
<h2 className="text-2xl font-black text-slate-900">{currentCategory?.id ? 'Edit Category' : 'New Category'}</h2>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">Configure category properties</p>
</div>
<button
onClick={() => setIsModalOpen(false)}
className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-slate-400 hover:text-rose-500 transition-all"
>
<X size={24} />
</button>
</div>
<form onSubmit={handleSubmit} className="p-10 space-y-8">
<div className="space-y-4">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] ml-2">Category Name</label>
<input
autoFocus
required
type="text"
value={currentCategory?.name || ''}
onChange={(e) => setCurrentCategory({...currentCategory, name: e.target.value})}
className="w-full bg-slate-50 border border-slate-100 rounded-2xl py-5 px-8 text-sm font-semibold outline-none focus:bg-white focus:ring-4 focus:ring-primary/5 focus:border-primary transition-all"
placeholder="e.g. FAST FOOD"
/>
</div>
<div className="space-y-4">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] ml-2">Description</label>
<textarea
value={currentCategory?.description || ''}
onChange={(e) => setCurrentCategory({...currentCategory, description: e.target.value})}
className="w-full bg-slate-50 border border-slate-100 rounded-2xl py-5 px-8 text-sm font-semibold outline-none focus:bg-white focus:ring-4 focus:ring-primary/5 focus:border-primary transition-all h-32 resize-none"
placeholder="Tell us more about this category..."
/>
</div>
<div className="flex items-center gap-4 bg-slate-50 p-6 rounded-3xl border border-slate-100">
<div className="flex-1">
<p className="text-sm font-black text-slate-800">Status</p>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Enable or disable category visibility</p>
</div>
<button
type="button"
onClick={() => setCurrentCategory({...currentCategory, active: !currentCategory.active})}
className={`w-16 h-8 rounded-full transition-all relative ${currentCategory?.active ? 'bg-primary' : 'bg-slate-300'}`}
>
<div className={`absolute top-1 w-6 h-6 bg-white rounded-full transition-all ${currentCategory?.active ? 'right-1' : 'left-1'}`} />
</button>
</div>
<div className="flex gap-4 pt-4">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="flex-1 py-5 rounded-2xl font-black text-xs uppercase tracking-widest text-slate-400 hover:bg-slate-50 transition-all border border-transparent hover:border-slate-100"
>
Cancel
</button>
<button
type="submit"
className="flex-1 bg-primary text-white py-5 rounded-2xl font-black text-xs uppercase tracking-widest shadow-xl shadow-primary/20 hover:scale-[1.02] active:scale-95 transition-all flex items-center justify-center gap-2"
>
<Check size={18} /> {currentCategory?.id ? 'Update Category' : 'Save Category'}
</button>
</div>
</form>
</motion.div>
</div>
)}
</div>
);
};
const StatCard = ({ icon: Icon, label, value, color }: any) => (
<div className="bg-white p-8 rounded-[2.8rem] border border-slate-100 shadow-sm flex items-center gap-8 group hover:shadow-2xl transition-all">
<div className={`w-16 h-16 rounded-[1.5rem] flex items-center justify-center group-hover:bg-primary group-hover:text-white transition-all ${color.split(' ')[0]} ${color.split(' ')[1]}`}>
<Icon size={32} />
</div>
<div>
<p className="text-3xl font-black text-slate-900 tracking-tighter">{value}</p>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">{label}</p>
</div>
</div>
);
export default Categories;

View File

@@ -0,0 +1,163 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import {
User,
Lock,
Eye,
EyeOff,
ArrowRight
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import CollegeLogo from '../assets/college-logo.png';
import RitChennaiLogo from '../assets/ritchennai.webp';
const Login = () => {
const [showPassword, setShowPassword] = useState(false);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate();
useEffect(() => {
sessionStorage.removeItem('isCounterLoggedIn');
sessionStorage.removeItem('counterUserName');
}, []);
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
if (username === 'krishna' && password === '12345678') {
sessionStorage.setItem('isCounterLoggedIn', 'true');
sessionStorage.setItem('counterUserName', 'Krishna');
navigate('/pos');
} else {
alert('Invalid credentials');
}
};
return (
<div className="min-h-screen bg-[#f8fafc] flex flex-col md:flex-row overflow-hidden font-inter">
{/* Left Section: Login Form */}
<div className="w-full md:w-[45%] lg:w-[40%] flex flex-col p-8 md:p-12 lg:p-16 relative z-10 bg-white shadow-2xl">
{/* Top Logo */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-10"
>
<img src={CollegeLogo} alt="College Logo" className="h-16 md:h-20 object-contain" />
</motion.div>
{/* Login Container */}
<motion.div
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
className="flex-1 flex flex-col justify-center max-w-sm mx-auto w-full"
>
<div className="text-center mb-10">
<h2 className="text-[10px] font-black tracking-[0.3em] text-[#1e293b] uppercase mb-2">
Counter Management System
</h2>
<div className="w-12 h-1 bg-primary mx-auto rounded-full"></div>
</div>
{/* Form */}
<form onSubmit={handleLogin} className="space-y-6">
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest pl-1">
Operator ID
</label>
<div className="relative group">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-[#94a3b8] group-focus-within:text-primary transition-colors">
<User size={18} />
</div>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
className="w-full bg-white border-2 border-[#e2e8f0] focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-sm font-semibold outline-none transition-all placeholder:text-[#cbd5e1]"
/>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center pl-1">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest">
Passcode
</label>
<button type="button" className="text-[10px] font-black text-primary uppercase hover:underline">
Forgot?
</button>
</div>
<div className="relative group">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-[#94a3b8] group-focus-within:text-primary transition-colors">
<Lock size={18} />
</div>
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="w-full bg-white border-2 border-[#e2e8f0] focus:border-primary rounded-2xl py-4 pl-12 pr-12 text-sm font-semibold outline-none transition-all placeholder:text-[#cbd5e1]"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 pr-4 flex items-center text-[#94a3b8] hover:text-[#64748b] transition-colors"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
<button
type="submit"
className="w-full bg-primary text-white rounded-2xl py-4 font-black text-sm flex items-center justify-center gap-3 shadow-xl shadow-primary/20 hover:scale-[1.02] active:scale-[0.98] transition-all overflow-hidden group relative"
>
<span className="relative z-10">CONNECT TO POS</span>
<ArrowRight size={18} className="relative z-10 group-hover:translate-x-1 transition-transform" />
<motion.div
className="absolute inset-0 bg-white/10"
initial={{ x: '-100%' }}
whileHover={{ x: '100%' }}
transition={{ duration: 0.6 }}
/>
</button>
</form>
</motion.div>
{/* Footer */}
<div className="mt-auto text-center">
<p className="text-[10px] font-black text-[#cbd5e1] uppercase tracking-[0.2em]">
© 2026 RITCHENNAI COUNTER SYSTEM
</p>
</div>
</div>
{/* Right Section: Visual (Building Photo) */}
<div className="hidden md:flex flex-1 relative bg-[#f8fafc] items-center justify-center p-8 overflow-hidden">
<div className="absolute top-20 right-20 w-64 h-64 bg-primary/5 rounded-full blur-3xl"></div>
<div className="absolute bottom-20 left-40 w-96 h-96 bg-primary/5 rounded-full blur-3xl"></div>
<motion.div
initial={{ opacity: 0, scale: 0.95, x: 50 }}
animate={{ opacity: 1, scale: 1, x: 0 }}
transition={{ duration: 0.8 }}
className="w-full h-full relative z-10"
>
<div className="w-full h-full overflow-hidden shadow-2xl border-[12px] border-white bg-white">
<img
src={RitChennaiLogo}
alt="RIT Chennai Building"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-tr from-primary/20 to-transparent"></div>
</div>
</motion.div>
</div>
</div>
);
};
export default Login;

View File

@@ -0,0 +1,251 @@
import React, { useState } from 'react';
import {
Search,
Calendar,
RefreshCw,
ShoppingBag,
CreditCard,
User,
CheckCircle2,
Clock,
ChevronRight
} from 'lucide-react';
import { motion } from 'framer-motion';
const MOCK_ORDERS = [
{ id: 13, displayId: '#13', date: '2026-04-08', amount: 320, status: 'PAID', items: 3 },
{ id: 12, displayId: '#12', date: '2026-04-08', amount: 1000, status: 'PAID', items: 1 },
{ id: 11, displayId: '#11', date: '2026-04-08', amount: 60, status: 'PAID', items: 2 },
{ id: 10, displayId: '#10', date: '2026-04-08', amount: 50, status: 'PAID', items: 1 },
{ id: 9, displayId: '#9', date: '2026-04-08', amount: 50, status: 'PAID', items: 1 },
{ id: 8, displayId: '#8', date: '2026-04-08', amount: 20, status: 'PAID', items: 1 },
];
const Orders = () => {
const [orders, setOrders] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedOrder, setSelectedOrder] = useState<any>(null);
const [activeTab, setActiveTab] = useState('ALL');
const [searchTerm, setSearchTerm] = useState('');
const fetchOrders = async () => {
setLoading(true);
try {
const host = window.location.hostname;
const paymentType = activeTab === 'ALL' ? '' : activeTab;
let url = `http://${host}:8080/api/orders/all?archived=false&paymentType=${paymentType}`;
if (searchTerm) {
url += `&search=${encodeURIComponent(searchTerm)}`;
}
const response = await fetch(url);
const data = await response.json();
const orderList = Array.isArray(data) ? data : (data.content || []);
setOrders(orderList);
if (orderList.length > 0 && !selectedOrder) {
setSelectedOrder(orderList[0]);
}
} catch (error) {
console.error('Error fetching orders:', error);
} finally {
setLoading(false);
}
};
React.useEffect(() => {
fetchOrders();
}, [activeTab, searchTerm]);
return (
<div className="h-full flex flex-col bg-slate-50 overflow-hidden">
{/* Sub Header / Tabs */}
<div className="bg-white border-b border-slate-200 px-8 flex items-center justify-between shadow-sm shrink-0">
<div className="flex gap-10">
{['ALL', 'CASH', 'UPI', 'CARD'].map((tab) => (
<button
key={tab}
onClick={() => {
setActiveTab(tab);
setSelectedOrder(null);
}}
className={`py-6 text-xs font-black uppercase tracking-[0.2em] relative transition-all flex items-center gap-2 ${
activeTab === tab ? 'text-primary' : 'text-slate-400 hover:text-slate-600'
}`}
>
{tab}
{activeTab === tab && <motion.div layoutId="activeOrderTab" className="absolute bottom-0 left-0 right-0 h-1 bg-primary rounded-t-full" />}
</button>
))}
</div>
</div>
{/* Main Content */}
<div className="flex-1 flex overflow-hidden">
{/* Left Sidebar: Order List */}
<div className="w-[400px] bg-white border-r border-slate-200 flex flex-col">
<div className="p-6 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
<h2 className="text-xs font-black text-slate-400 uppercase tracking-widest">Order Feed</h2>
<button onClick={fetchOrders} className="text-primary hover:rotate-180 transition-all duration-500">
<RefreshCw size={16} />
</button>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar p-6 space-y-4">
{loading ? (
<div className="h-40 flex items-center justify-center">
<div className="w-8 h-8 border-4 border-primary/20 border-t-primary rounded-full animate-spin"></div>
</div>
) : orders.length > 0 ? (
orders.map((order) => (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
key={order.id}
onClick={() => setSelectedOrder(order)}
className={`p-6 rounded-[2rem] border-2 transition-all cursor-pointer group ${
selectedOrder?.id === order.id
? 'bg-primary/5 border-primary shadow-xl shadow-primary/5 scale-[1.02]'
: 'bg-white border-slate-100/50 hover:border-primary/20 hover:shadow-lg'
}`}
>
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="font-black text-slate-900 group-hover:text-primary transition-colors text-lg">#{order.displayOrderId || order.id}</h3>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{new Date(order.createdAt).toLocaleTimeString()}</p>
</div>
<span className="text-xl font-black text-slate-900 tracking-tighter">{order.totalAmount}</span>
</div>
<div className="flex justify-between items-center">
<span className={`px-3 py-1 rounded-lg text-[9px] font-black uppercase tracking-widest ${order.status === 'PAID' ? 'bg-emerald-50 text-emerald-600' : 'bg-amber-50 text-amber-600'}`}>
{order.status}
</span>
<div className="w-8 h-8 rounded-xl bg-slate-50 flex items-center justify-center text-slate-400 group-hover:bg-primary group-hover:text-white transition-all">
<ChevronRight size={16} />
</div>
</div>
</motion.div>
))
) : (
<div className="h-64 flex flex-col items-center justify-center text-slate-300 gap-4 opacity-50">
<ShoppingBag size={48} />
<p className="font-black uppercase tracking-widest text-[10px]">No orders found in {activeTab}</p>
</div>
)}
</div>
</div>
{/* Right Section: Order Detail */}
<div className="flex-1 overflow-y-auto p-12 custom-scrollbar bg-white">
{selectedOrder ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="max-w-4xl mx-auto"
>
<div className="flex items-center justify-between mb-12">
<div className="flex items-center gap-8">
<div className="w-20 h-20 bg-primary/5 rounded-[2rem] flex items-center justify-center text-primary border border-primary/10">
<ShoppingBag size={40} />
</div>
<div>
<div className="flex items-center gap-4">
<h1 className="text-4xl font-black text-slate-900 tracking-tight">Order #{selectedOrder.displayOrderId || selectedOrder.id}</h1>
<span className="bg-emerald-50 text-emerald-600 px-6 py-2 rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] border border-emerald-100 shadow-sm">{selectedOrder.status}</span>
</div>
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mt-2 opacity-60">Reference ID: {selectedOrder.orderNumber}</p>
</div>
</div>
<div className="text-right">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] mb-1">Created At</p>
<p className="text-sm font-black text-slate-900">{new Date(selectedOrder.createdAt).toLocaleString()}</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
<div className="bg-slate-50/50 p-8 rounded-[2.5rem] border border-slate-100/50 space-y-3">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">Payment Type</p>
<p className="text-md font-black text-slate-800 flex items-center gap-3 uppercase tracking-tight">
<CreditCard size={20} className="text-primary" /> {selectedOrder.paymentMethod}
</p>
</div>
<div className="bg-slate-50/50 p-8 rounded-[2.5rem] border border-slate-100/50 space-y-3">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">Customer</p>
<p className="text-md font-black text-slate-800 flex items-center gap-3">
<User size={20} className="text-primary" /> {selectedOrder.user?.name || 'Walk-in Customer'}
</p>
</div>
<div className="bg-slate-50/50 p-8 rounded-[2.5rem] border border-slate-100/50 space-y-3">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">Order Type</p>
<p className="text-md font-black text-slate-800 flex items-center gap-3 uppercase tracking-widest">
{selectedOrder.orderType.replace('_', ' ')}
</p>
</div>
</div>
<div className="mb-12">
<div className="flex justify-between items-center mb-6">
<h3 className="text-xs font-black text-slate-400 uppercase tracking-[0.2em]">Item List</h3>
<span className="text-xs font-bold text-slate-400 leading-none">{selectedOrder.items?.length || 0} ITEMS</span>
</div>
<div className="space-y-4">
{selectedOrder.items?.map((item: any, idx: number) => (
<div key={idx} className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm flex items-center justify-between group hover:border-primary/20 transition-all">
<div className="flex items-center gap-6">
<div className="w-16 h-16 bg-slate-50 rounded-2xl flex items-center justify-center font-black text-slate-400 text-sm group-hover:bg-primary/5 group-hover:text-primary transition-all">
{item.productName?.substring(0, 2).toUpperCase()}
</div>
<div>
<h4 className="font-black text-slate-800 uppercase tracking-tight text-md">{item.productName}</h4>
<p className="text-[10px] font-bold text-slate-400 mt-1 uppercase tracking-widest">{item.stallName}</p>
</div>
</div>
<div className="text-right">
<p className="text-xl font-black text-slate-900 tracking-tighter">{item.price * item.quantity}</p>
<p className="text-[10px] font-bold text-slate-400 mt-0.5 uppercase tracking-widest">{item.quantity}x @ {item.price}</p>
</div>
</div>
))}
</div>
</div>
<div className="flex justify-end pt-8 border-t border-slate-100">
<div className="bg-primary p-1 rounded-[3rem] shadow-2xl shadow-primary/30 min-w-[400px]">
<div className="bg-white p-10 rounded-[2.8rem] space-y-6">
<div className="flex justify-between items-center opacity-40">
<span className="text-xs font-black uppercase tracking-widest">Subtotal</span>
<span className="text-sm font-black tracking-tight">{selectedOrder.totalAmount}</span>
</div>
<div className="h-[1px] bg-slate-100" />
<div className="flex justify-between items-center">
<span className="text-lg font-black text-slate-900 tracking-tight uppercase">Order Total</span>
<span className="text-4xl font-black text-primary tracking-tighter">{selectedOrder.totalAmount}</span>
</div>
</div>
</div>
</div>
</motion.div>
) : (
<div className="h-full flex flex-col items-center justify-center text-slate-200 gap-6 opacity-40">
<div className="w-24 h-24 bg-slate-50 rounded-[2.5rem] flex items-center justify-center">
<ShoppingBag size={48} />
</div>
<p className="font-black uppercase tracking-[0.3em] text-[10px]">Select an order to view details</p>
</div>
)}
</div>
</div>
<style dangerouslySetInnerHTML={{ __html: `
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
` }} />
</div>
);
};
export default Orders;

View File

@@ -0,0 +1,352 @@
import React, { useState, useEffect, useMemo } from 'react';
import {
Search,
ShoppingBag,
Trash2,
Plus,
Minus,
UtensilsCrossed,
Coffee,
IceCream,
Pizza,
Cake,
Phone,
Loader2
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
interface Product {
id: number;
name: string;
price: number;
stock: number;
category: string;
imageData?: string;
}
const getCategoryIcon = (category: string) => {
const cat = category.toUpperCase();
if (cat.includes('ICE')) return IceCream;
if (cat.includes('JUICE') || cat.includes('DRINK')) return UtensilsCrossed;
if (cat.includes('COFFEE') || cat.includes('TEA') || cat.includes('BISCUIT')) return Coffee;
if (cat.includes('PIZZA') || cat.includes('SNACK')) return Pizza;
if (cat.includes('CAKE') || cat.includes('SWEET') || cat.includes('CHOCO')) return Cake;
return UtensilsCrossed;
};
const POS = () => {
const [products, setProducts] = useState<Product[]>([]);
const [categories, setCategories] = useState<string[]>(['ALL']);
const [loading, setLoading] = useState(true);
const [activeCategory, setActiveCategory] = useState('ALL');
const [cart, setCart] = useState<any[]>([]);
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
fetchInitialData();
}, []);
const fetchInitialData = async () => {
try {
setLoading(true);
const host = window.location.hostname;
const [prodRes, catRes] = await Promise.all([
fetch(`http://${host}:8080/api/products?size=1000`),
fetch(`http://${host}:8080/api/products/categories`)
]);
if (prodRes.ok) {
const data = await prodRes.json();
setProducts(data.content || []);
}
if (catRes.ok) {
const data = await catRes.json();
setCategories(['ALL', ...data]);
}
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
};
const filteredProducts = useMemo(() => {
return products.filter(p => {
if (!p) return false;
const search = (searchQuery || '').toLowerCase();
const name = (p.name || '').toLowerCase();
const matchesSearch = name.includes(search);
const matchesCategory = activeCategory === 'ALL' || p.category === activeCategory;
return matchesSearch && matchesCategory;
});
}, [products, searchQuery, activeCategory]);
const addToCart = (product: Product) => {
const existing = cart.find(item => item.id === product.id);
if (existing) {
setCart(cart.map(item =>
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
));
} else {
setCart([...cart, { ...product, quantity: 1 }]);
}
};
const updateQuantity = (id: number, delta: number) => {
setCart(cart.map(item => {
if (item.id === id) {
const newQty = Math.max(1, item.quantity + delta);
return { ...item, quantity: newQty };
}
return item;
}));
};
const removeFromCart = (id: number) => {
setCart(cart.filter(item => item.id !== id));
};
const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const handleCompleteOrder = async () => {
if (cart.length === 0) return;
setLoading(true);
try {
const host = window.location.hostname;
const orderData = {
userId: 1, // Using test user ID
totalAmount: total,
paymentMethod: "CASH", // Default to CASH for now
status: "COMPLETED",
items: cart.map(item => ({
productId: item.id,
productName: item.name,
price: item.price,
quantity: item.quantity
}))
};
const response = await fetch(`http://${host}:8080/api/orders`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(orderData),
});
if (response.ok) {
alert("Order placed successfully! Stock has been updated.");
setCart([]);
fetchInitialData();
} else {
let errorMessage = "Unknown error";
try {
const err = await response.json();
errorMessage = err.error || err.message || JSON.stringify(err);
} catch (e) {
errorMessage = await response.text() || response.statusText;
}
alert(`Failed to place order: ${errorMessage}`);
}
} catch (error) {
console.error("Order error:", error);
alert("Network error while placing order.");
} finally {
setLoading(false);
}
};
return (
<div className="h-[calc(100vh-64px)] flex overflow-hidden">
{/* Left Section: Menu */}
<div className="flex-1 flex flex-col bg-slate-50 overflow-hidden">
{/* Categories Bar */}
<div className="p-4 bg-white border-b border-slate-200 overflow-x-auto no-scrollbar">
<div className="flex gap-4">
{categories.map((catName) => {
const Icon = getCategoryIcon(catName);
return (
<button
key={catName}
onClick={() => setActiveCategory(catName)}
className={`flex items-center gap-2 px-6 py-3 rounded-xl text-xs font-black transition-all border-2 whitespace-nowrap ${
activeCategory === catName
? 'bg-primary text-white border-primary shadow-lg shadow-primary/20'
: 'bg-white text-slate-400 border-indigo-50 hover:border-primary/30'
}`}
>
<Icon size={16} />
{catName}
</button>
);
})}
</div>
</div>
{/* Search and Products Grid */}
<div className="flex-1 overflow-y-auto p-6 flex flex-col gap-6 custom-scrollbar">
<div className="relative group max-w-2xl">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-primary transition-colors" size={20} />
<input
type="text"
placeholder="Search Product..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-12 pr-4 py-4 bg-white border border-slate-200 rounded-2xl text-sm font-semibold outline-none focus:ring-4 focus:ring-primary/5 focus:border-primary transition-all shadow-sm"
/>
</div>
{loading ? (
<div className="flex-1 flex items-center justify-center">
<Loader2 className="animate-spin text-primary" size={48} />
</div>
) : filteredProducts.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center text-slate-400">
<ShoppingBag size={64} className="mb-4 opacity-20" />
<p className="font-bold">No products found</p>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{filteredProducts.map((product) => (
<motion.div
whileHover={{ y: -4 }}
whileTap={{ scale: 0.98 }}
key={product.id}
onClick={() => addToCart(product)}
className="bg-white rounded-[2rem] p-5 border border-slate-100 shadow-sm hover:shadow-xl hover:border-primary/20 transition-all cursor-pointer group flex flex-col relative"
>
<div className="absolute top-4 right-4 z-10">
<span className={`px-2.5 py-1 rounded-lg text-[10px] font-black border uppercase tracking-wider ${
(product.stock || 0) < 20 ? 'bg-rose-50 text-rose-500 border-rose-100' : 'bg-emerald-50 text-emerald-500 border-emerald-100'
}`}>
{product.stock || 0} left
</span>
</div>
<div className="aspect-square bg-slate-50 rounded-2xl mb-4 flex items-center justify-center text-slate-200 group-hover:bg-primary/5 transition-colors overflow-hidden">
{product.imageData ? (
<img src={product.imageData} alt={product.name} className="w-full h-full object-cover" />
) : (
<ShoppingBag size={48} />
)}
</div>
<h3 className="text-xs font-black text-slate-800 leading-snug mb-1 line-clamp-2 uppercase">
{product.name}
</h3>
<div className="mt-auto">
<p className="text-lg font-black text-primary">{product.price}</p>
</div>
</motion.div>
))}
</div>
)}
</div>
</div>
{/* Right Section: Current Order */}
<div className="w-[450px] bg-white border-l border-slate-200 flex flex-col shadow-[-10px_0_30px_rgba(0,0,0,0.02)]">
<div className="p-6 border-b border-slate-100 flex justify-between items-center">
<div>
<h2 className="text-xl font-black text-slate-900 tracking-tight">Current Order</h2>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest flex items-center gap-1.5 mt-0.5">
<Phone size={10} className="text-primary" /> +91 9043941910
</p>
</div>
<div className="flex items-center gap-3">
<button className="px-4 py-2 bg-slate-50 text-slate-600 rounded-xl text-[10px] font-black uppercase hover:bg-slate-100 transition-colors">View Orders</button>
<span className="bg-primary text-white text-[10px] font-black px-3 py-1.5 rounded-lg uppercase shadow-lg shadow-primary/20">{cart.length} items</span>
</div>
</div>
<div className="flex-1 overflow-y-auto p-6 flex flex-col gap-4 custom-scrollbar">
<AnimatePresence mode="popLayout">
{cart.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center text-center text-slate-400 gap-4">
<div className="w-20 h-20 bg-slate-50 rounded-3xl flex items-center justify-center">
<ShoppingBag size={40} className="text-slate-200" />
</div>
<div>
<h3 className="font-black text-slate-600">Cart is empty</h3>
<p className="text-xs font-medium">Select products to start billing</p>
</div>
</div>
) : (
cart.map((item) => (
<motion.div
layout
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
key={item.id}
className="bg-[#ebfaf4] border border-[#d1f4e6] rounded-[2rem] p-5 relative group"
>
<div className="flex justify-between items-start mb-4">
<div className="flex-1 pr-12">
<h4 className="text-[11px] font-black text-slate-800 uppercase tracking-tight leading-tight">{item.name}</h4>
<p className="text-[10px] font-bold text-[#42ab7e] mt-1 uppercase">{item.price} each</p>
</div>
<button
onClick={() => removeFromCart(item.id)}
className="w-9 h-9 bg-white text-rose-500 rounded-xl flex items-center justify-center shadow-sm border border-rose-50 hover:bg-rose-50 transition-colors"
>
<Trash2 size={16} />
</button>
</div>
<div className="flex justify-between items-center">
<div className="flex items-center bg-white rounded-xl p-1 shadow-sm border border-slate-50">
<button
onClick={() => updateQuantity(item.id, -1)}
className="w-8 h-8 flex items-center justify-center text-slate-400 hover:text-primary transition-colors"
>
<Minus size={14} />
</button>
<span className="w-10 text-center font-black text-sm text-slate-900">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, 1)}
className="w-8 h-8 flex items-center justify-center text-slate-400 hover:text-primary transition-colors"
>
<Plus size={14} />
</button>
</div>
<div className="text-right">
<p className="text-lg font-black text-slate-900 tracking-tight">{item.price * item.quantity}</p>
</div>
</div>
</motion.div>
))
)}
</AnimatePresence>
</div>
<div className="p-8 bg-slate-50 border-t border-slate-200 shadow-[0_-10px_40px_rgba(0,0,0,0.03)] rounded-t-[3rem]">
<div className="flex justify-between items-center mb-6">
<span className="text-lg font-black text-slate-900">Total:</span>
<span className="text-4xl font-black text-primary tracking-tighter">{total}</span>
</div>
<button
disabled={cart.length === 0}
onClick={handleCompleteOrder}
className="w-full bg-primary text-white py-5 rounded-[2rem] font-black text-sm uppercase tracking-[0.2em] shadow-2xl shadow-primary/20 hover:scale-[1.02] active:scale-[0.98] transition-all disabled:opacity-50 disabled:scale-100 disabled:shadow-none"
>
Complete Order
</button>
</div>
</div>
<style dangerouslySetInnerHTML={{ __html: `
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.custom-scrollbar::-webkit-scrollbar { width: 5px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; }
` }} />
</div>
);
};
export default POS;

View File

@@ -0,0 +1,412 @@
import React, { useState, useEffect, useRef } from 'react';
import {
Plus,
X,
Search,
RefreshCw,
Edit2,
Trash2,
Database,
ShoppingBag,
Package,
Clock,
Check,
Power,
PowerOff,
Tag,
Barcode,
Image as ImageIcon
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import Pagination from '../components/Pagination';
interface ProductSession {
id?: number;
dayOfWeek: string;
active: boolean;
startTime: string;
endTime: string;
}
interface Product {
id?: number;
productId: string;
name: string;
category: string;
description: string;
basePrice: number;
price: number;
offerPrice: number;
discountPercent: number;
discountAmount: number;
counter: string;
tag: string;
parcelCharges: number;
barcode: string;
attributesOptional: boolean;
veg: boolean;
hasAllergy: boolean;
parcelNotAllowed: boolean;
sessionOptional: boolean;
sessions: ProductSession[];
imageData: string;
active: boolean;
stock: number;
}
const DAYS = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY'];
const getDefaultSessions = (): ProductSession[] =>
DAYS.map(day => ({ dayOfWeek: day, active: true, startTime: '00:00', endTime: '23:59' }));
const emptyProduct: Product = {
productId: '',
name: '',
category: '',
description: '',
basePrice: 0,
price: 0,
offerPrice: 0,
discountPercent: 0,
discountAmount: 0,
counter: '',
tag: '',
parcelCharges: 0,
barcode: '',
attributesOptional: false,
veg: false,
hasAllergy: false,
parcelNotAllowed: false,
sessionOptional: false,
sessions: getDefaultSessions(),
imageData: '',
active: true,
stock: 0
};
const Products = () => {
const [products, setProducts] = useState<Product[]>([]);
const [categories, setCategories] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
const [formData, setFormData] = useState<Product>(emptyProduct);
const [searchTerm, setSearchTerm] = useState('');
const [currentPage, setCurrentPage] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [totalElements, setTotalElements] = useState(0);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const catParam = params.get('category');
if (catParam) {
setSearchTerm(catParam);
}
}, []);
useEffect(() => {
fetchProducts();
fetchCategories();
}, [currentPage, pageSize]);
const fetchProducts = async () => {
setLoading(true);
try {
const host = window.location.hostname;
const response = await fetch(`http://${host}:8080/api/products?page=${currentPage}&size=${pageSize}`);
const data = await response.json();
if (data && data.content) {
setProducts(data.content);
setTotalElements(data.totalElements);
} else if (Array.isArray(data)) {
setProducts(data);
setTotalElements(data.length);
}
} catch (error) {
console.error('Error fetching products:', error);
} finally {
setLoading(false);
}
};
const fetchCategories = async () => {
try {
const host = window.location.hostname;
const response = await fetch(`http://${host}:8080/api/base-items`);
const data = await response.json();
setCategories(Array.isArray(data) ? data : (data.content || []));
} catch (error) {
console.error('Error fetching categories:', error);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const host = window.location.hostname;
const url = editingProduct?.id
? `http://${host}:8080/api/products/${editingProduct.id}`
: `http://${host}:8080/api/products`;
const method = editingProduct?.id ? 'PUT' : 'POST';
try {
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (response.ok) {
setIsModalOpen(false);
fetchProducts();
}
} catch (error) {
console.error('Error saving product:', error);
}
};
const handleDelete = async (product: Product) => {
if (!window.confirm(`Delete ${product.name}?`)) return;
try {
const host = window.location.hostname;
const response = await fetch(`http://${host}:8080/api/products/${product.id}`, { method: 'DELETE' });
if (response.ok) fetchProducts();
} catch (error) {
console.error('Error deleting:', error);
}
};
const handleToggleStock = async (product: Product) => {
try {
const host = window.location.hostname;
const response = await fetch(`http://${host}:8080/api/products/${product.id}/toggle-stock`, { method: 'PATCH' });
if (response.ok) fetchProducts();
} catch (error) {
console.error('Error toggling stock:', error);
}
};
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => setFormData({ ...formData, imageData: reader.result as string });
reader.readAsDataURL(file);
}
};
const filteredProducts = products.filter(p =>
(p.name || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
(p.category || '').toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="h-full flex flex-col bg-slate-50 overflow-hidden font-inter">
{/* Search & Header */}
<div className="px-8 py-6 bg-white border-b border-slate-100 flex flex-col md:flex-row items-center justify-between gap-6 shrink-0 shadow-sm relative z-10">
<div>
<h1 className="text-3xl font-black text-slate-900 tracking-tight leading-none mb-1">Products</h1>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">Manage catalog & stock details</p>
</div>
<div className="flex-1 max-w-2xl px-6 py-3 bg-slate-50 border border-slate-100 rounded-[2rem] flex items-center gap-4 group focus-within:bg-white focus-within:ring-4 focus-within:ring-primary/5 transition-all">
<Search size={22} className="text-slate-400 group-focus-within:text-primary transition-colors" />
<input
type="text"
placeholder="Search products by name or category..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-transparent border-none outline-none text-sm font-semibold text-slate-900"
/>
</div>
<button
onClick={() => {
setEditingProduct(null);
setFormData(emptyProduct);
setIsModalOpen(true);
}}
className="bg-primary text-white px-10 py-4 rounded-[2rem] font-black text-xs uppercase tracking-widest flex items-center gap-3 shadow-2xl shadow-primary/30 hover:scale-[1.02] active:scale-95 transition-all w-full md:w-auto justify-center"
>
<Plus size={18} /> Add Product
</button>
</div>
{/* Grid Content */}
<div className="flex-1 overflow-y-auto custom-scrollbar p-8">
{loading ? (
<div className="h-full flex flex-col items-center justify-center gap-4">
<div className="w-12 h-12 border-4 border-primary/20 border-t-primary rounded-full animate-spin"></div>
<p className="text-xs font-black text-slate-400 uppercase tracking-widest">Loading Catalog...</p>
</div>
) : filteredProducts.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-8">
<AnimatePresence mode="popLayout">
{filteredProducts.map((prod) => (
<motion.div
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9 }}
key={prod.id}
className="bg-white rounded-[3rem] border border-slate-100 shadow-sm hover:shadow-2xl transition-all group flex flex-col overflow-hidden relative"
>
<div className="aspect-[4/3] bg-slate-50 relative overflow-hidden group-hover:bg-primary/5 transition-colors">
{prod.imageData ? (
<img src={prod.imageData} alt={prod.name} className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110" />
) : (
<div className="w-full h-full flex items-center justify-center text-slate-200 group-hover:text-primary/20 transition-all">
<ShoppingBag size={80} />
</div>
)}
<div className="absolute top-6 left-6 flex flex-col gap-2">
<span className="bg-white/90 backdrop-blur px-3 py-1 rounded-xl text-[9px] font-black uppercase text-primary border border-primary/10 shadow-sm">{prod.price}</span>
</div>
<div className="absolute top-6 right-6 flex flex-col gap-2 opacity-0 group-hover:opacity-100 transition-all translate-x-4 group-hover:translate-x-0">
<button
onClick={() => { setEditingProduct(prod); setFormData(prod); setIsModalOpen(true); }}
className="w-10 h-10 bg-white rounded-xl flex items-center justify-center text-slate-400 hover:text-primary shadow-xl transition-all"
>
<Edit2 size={16} />
</button>
<button
onClick={() => handleDelete(prod)}
className="w-10 h-10 bg-white rounded-xl flex items-center justify-center text-slate-400 hover:text-rose-500 shadow-xl transition-all"
>
<Trash2 size={16} />
</button>
</div>
</div>
<div className="p-8 flex flex-col gap-6 flex-1">
<div>
<div className="flex items-center gap-2 mb-2">
<span className="bg-primary/5 text-primary px-2 py-0.5 rounded text-[8px] font-black uppercase tracking-widest">{prod.category}</span>
<span className={`px-2 py-0.5 rounded text-[8px] font-black uppercase tracking-widest ${prod.stock > 0 ? 'bg-emerald-50 text-emerald-600' : 'bg-rose-50 text-rose-600'}`}>
{prod.stock > 0 ? `Stock: ${prod.stock}` : 'Out of Stock'}
</span>
</div>
<h3 className="text-lg font-black text-slate-900 uppercase tracking-tight leading-tight group-hover:text-primary transition-colors">{prod.name}</h3>
</div>
<div className="flex gap-3 mt-auto pt-4 border-t border-slate-50">
<button
onClick={() => handleToggleStock(prod)}
className={`flex-1 py-3 px-4 rounded-2xl text-[10px] font-black uppercase tracking-widest flex items-center justify-center gap-2 transition-all ${
prod.stock > 0
? 'bg-rose-50 text-rose-600 hover:bg-rose-600 hover:text-white'
: 'bg-emerald-50 text-emerald-600 hover:bg-emerald-600 hover:text-white'
}`}
>
<Database size={12} /> {prod.stock > 0 ? 'Out of Stock' : 'In Stock'}
</button>
</div>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-slate-300 gap-6 opacity-40">
<div className="w-24 h-24 bg-slate-50 rounded-[3rem] flex items-center justify-center">
<Package size={48} />
</div>
<p className="font-black uppercase tracking-[0.3em] text-[10px]">No products matched your search</p>
</div>
)}
</div>
<Pagination
currentPage={currentPage}
pageSize={pageSize}
totalElements={totalElements}
onPageChange={setCurrentPage}
onPageSizeChange={(newSize) => { setPageSize(newSize); setCurrentPage(0); }}
/>
{/* CRUD Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-8">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} onClick={() => setIsModalOpen(false)} className="absolute inset-0 bg-slate-900/60 backdrop-blur-md" />
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="relative bg-white w-full max-w-5xl rounded-[3rem] shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
<div className="p-10 border-b border-slate-100 flex items-center justify-between shrink-0 bg-white">
<div>
<h2 className="text-3xl font-black text-slate-900">{editingProduct ? 'Edit Product' : 'Add New Product'}</h2>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">Configure catalog & inventory data</p>
</div>
<button onClick={() => setIsModalOpen(false)} className="w-14 h-14 bg-slate-50 rounded-2xl flex items-center justify-center text-slate-400 hover:text-rose-500 transition-all"><X size={32} /></button>
</div>
<form onSubmit={handleSubmit} className="p-10 overflow-y-auto custom-scrollbar space-y-12">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
<div className="space-y-8">
<div className="group">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-4 mb-2 block">Product Identity</label>
<div className="grid grid-cols-2 gap-4">
<input required type="text" placeholder="Prod Name*" value={formData.name} onChange={(e) => setFormData({...formData, name: e.target.value})} className="col-span-2 bg-slate-50 border border-slate-100 rounded-2xl py-5 px-8 text-sm font-semibold outline-none focus:bg-white focus:ring-4 focus:ring-primary/5 focus:border-primary transition-all" />
<input type="text" placeholder="Prod ID" value={formData.productId} onChange={(e) => setFormData({...formData, productId: e.target.value})} className="bg-slate-50 border border-slate-100 rounded-2xl py-5 px-8 text-sm font-semibold outline-none focus:bg-white focus:ring-4 focus:ring-primary/5 focus:border-primary transition-all" />
<select required value={formData.category} onChange={(e) => setFormData({...formData, category: e.target.value})} className="bg-slate-50 border border-slate-100 rounded-2xl py-5 px-8 text-sm font-semibold outline-none focus:bg-white focus:ring-4 focus:ring-primary/5 focus:border-primary transition-all appearance-none cursor-pointer">
<option value="">Category*</option>
{categories.map(c => <option key={c.id} value={c.name}>{c.name}</option>)}
</select>
</div>
</div>
<div className="group">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-4 mb-2 block">Pricing & Value</label>
<div className="grid grid-cols-2 gap-4">
<div className="relative">
<span className="absolute left-6 top-1/2 -translate-y-1/2 text-slate-300 font-bold"></span>
<input type="number" placeholder="MRP Price" value={formData.price} onChange={(e) => setFormData({...formData, price: parseFloat(e.target.value) || 0})} className="w-full bg-slate-50 border border-slate-100 rounded-2xl py-5 pl-10 pr-8 text-sm font-black outline-none focus:bg-white focus:ring-4 focus:ring-primary/5 focus:border-primary transition-all text-primary" />
</div>
<input type="number" placeholder="Inventory Count" value={formData.stock} onChange={(e) => setFormData({...formData, stock: parseInt(e.target.value) || 0})} className="bg-slate-50 border border-slate-100 rounded-2xl py-5 px-8 text-sm font-bold outline-none focus:bg-white focus:ring-4 focus:ring-primary/5 focus:border-primary transition-all" />
</div>
</div>
</div>
<div className="space-y-8">
<div className="group">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-4 mb-2 block">Product Visual</label>
<div
onClick={() => fileInputRef.current?.click()}
className="h-48 bg-slate-50 border-4 border-dashed border-slate-100 rounded-[3rem] flex flex-col items-center justify-center text-slate-300 hover:border-primary/20 hover:bg-primary/5 transition-all cursor-pointer overflow-hidden relative"
>
{formData.imageData ? (
<img src={formData.imageData} className="w-full h-full object-contain" />
) : (
<>
<ImageIcon size={48} className="mb-2" />
<span className="text-[10px] font-black uppercase tracking-widest">Select Image</span>
</>
)}
<input ref={fileInputRef} type="file" className="hidden" onChange={handleImageChange} />
</div>
</div>
<div className="flex flex-wrap gap-4">
<button type="button" onClick={() => setFormData({...formData, veg: !formData.veg})} className={`flex-1 py-4 px-6 rounded-2xl text-[10px] font-black uppercase tracking-widest border transition-all ${formData.veg ? 'bg-emerald-50 border-emerald-100 text-emerald-600' : 'bg-slate-50 border-slate-100 text-slate-300'}`}>VEG ONLY</button>
<button type="button" onClick={() => setFormData({...formData, active: !formData.active})} className={`flex-1 py-4 px-6 rounded-2xl text-[10px] font-black uppercase tracking-widest border transition-all ${formData.active ? 'bg-indigo-50 border-indigo-100 text-indigo-600' : 'bg-slate-50 border-slate-100 text-slate-300'}`}>PUBLISHED</button>
</div>
</div>
</div>
<div className="flex gap-4 pt-10 sticky bottom-0 bg-white">
<button type="button" onClick={() => setIsModalOpen(false)} className="flex-1 py-6 rounded-[2rem] font-black text-xs uppercase tracking-widest text-slate-400 hover:bg-slate-50 transition-all border border-transparent hover:border-slate-100">Discard Changes</button>
<button type="submit" className="flex-[2] bg-primary text-white py-6 rounded-[2rem] font-black text-xs uppercase tracking-widest shadow-2xl shadow-primary/30 hover:scale-[1.02] active:scale-95 transition-all flex items-center justify-center gap-2"><Check size={20} /> {editingProduct ? 'Save Updates' : 'Publish Product'}</button>
</div>
</form>
</motion.div>
</div>
)}
<style dangerouslySetInnerHTML={{ __html: `
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
` }} />
</div>
);
};
export default Products;

1
counter-frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,20 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
primary: "#231651",
secondary: "#64748b",
background: "#f8fafc",
},
fontFamily: {
inter: ['Inter', 'sans-serif'],
}
},
},
plugins: [],
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})