QR Added to the orders screens.

This commit is contained in:
Sidharth Prabhu
2026-04-20 09:54:52 +05:30
parent 12144d41c1
commit 96bd334e3a
14 changed files with 394 additions and 73 deletions

View File

@@ -15,6 +15,7 @@
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.7",
"lucide-react": "^1.7.0",
"qrcode.react": "^4.2.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.14.0",
@@ -3516,6 +3517,15 @@
"node": ">=6"
}
},
"node_modules/qrcode.react": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",

View File

@@ -17,6 +17,7 @@
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.7",
"lucide-react": "^1.7.0",
"qrcode.react": "^4.2.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.14.0",

View File

@@ -15,6 +15,7 @@ import {
ArrowLeft
} from 'lucide-react';
import { format, subDays } from 'date-fns';
import { QRCodeCanvas } from 'qrcode.react';
import Pagination from '../components/Pagination';
interface OrderItem {
@@ -336,7 +337,7 @@ const ArchivedOrders: React.FC = () => {
{/* Bottom Totals Summary */}
<div className="bg-slate-900 p-8 text-white">
<div className="flex justify-between">
<div className="flex gap-12">
<div className="flex gap-12 items-center">
<div>
<div className="text-[10px] text-slate-500 uppercase font-black tracking-widest mb-2">Unique Items</div>
<div className="text-2xl font-black">{selectedOrder.items.length}</div>
@@ -347,6 +348,32 @@ const ArchivedOrders: React.FC = () => {
<Clock size={12} /> Closed File
</div>
</div>
{/* QR Verification with Status Blur */}
<div className="flex items-center gap-5 pl-12 border-l border-white/10">
<div className="relative flex items-center justify-center bg-white/5 p-2 rounded-xl border border-white/10 group/qr transition-all">
<div className={`transition-all duration-700 ${['COMPLETED', 'CANCELLED'].includes(selectedOrder.status.toUpperCase()) ? 'blur-[3px] opacity-20' : ''}`}>
<QRCodeCanvas
value={selectedOrder.orderNumber}
size={64}
level="H"
bgColor="transparent"
fgColor="#ffffff"
/>
</div>
{['COMPLETED', 'CANCELLED'].includes(selectedOrder.status.toUpperCase()) && (
<div className="absolute inset-0 flex items-center justify-center rotate-[-12deg] pointer-events-none">
<span className={`text-[10px] font-black text-white px-3 py-1 rounded-lg border border-white/20 shadow-2xl uppercase tracking-[0.2em] backdrop-blur-md ${selectedOrder.status.toUpperCase() === 'COMPLETED' ? 'bg-emerald-500/80' : 'bg-rose-500/80'}`}>
{selectedOrder.status.toUpperCase() === 'COMPLETED' ? 'DELIVERED' : 'EXPIRED'}
</span>
</div>
)}
</div>
<div>
<div className="text-[9px] text-slate-500 uppercase font-black tracking-widest mb-0.5">Verification</div>
<div className="text-[10px] font-black text-white uppercase tracking-tighter opacity-80">Ledger Sync QR</div>
</div>
</div>
</div>
<div className="text-right flex flex-col gap-1 min-w-[250px]">
<div className="text-[10px] text-slate-500 uppercase font-black tracking-widest mb-1">Final Settlement</div>

View File

@@ -328,15 +328,15 @@ const Dashboard = () => {
<div className="col-span-12 lg:col-span-5 bg-white rounded-[32px] border border-slate-100 shadow-sm p-8 flex flex-col">
<h3 className="text-sm font-black text-slate-800 tracking-tight mb-8 uppercase tracking-widest">Business Intelligence</h3>
<div className="space-y-4 flex-1">
{insights.map((insight: string, idx: number) => (
{insights.map((insight: any, idx: number) => (
<motion.div
initial={{ x: -20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: idx * 0.1 }}
key={idx}
className="p-5 rounded-2xl border border-slate-100 bg-slate-50/50 hover:bg-white hover:shadow-md hover:border-transparent transition-all cursor-default"
className={`p-5 rounded-2xl border transition-all cursor-default ${insight.color || 'border-slate-100 bg-slate-50/50 hover:bg-white hover:shadow-md hover:border-transparent'}`}
>
<p className="text-[11px] font-black text-slate-600 leading-relaxed uppercase tracking-tight">{insight}</p>
<p className="text-[11px] font-black text-slate-600 leading-relaxed uppercase tracking-tight">{insight.text || insight}</p>
</motion.div>
))}
</div>

View File

@@ -15,9 +15,11 @@ import {
Check,
X as XIcon,
RotateCcw,
Edit2
Edit2,
Printer
} from 'lucide-react';
import { format } from 'date-fns';
import { QRCodeCanvas } from 'qrcode.react';
import Pagination from '../components/Pagination';
interface OrderItem {
@@ -75,6 +77,8 @@ const Orders: React.FC = () => {
const [editSearchQuery, setEditSearchQuery] = useState('');
const [editingItems, setEditingItems] = useState<OrderItem[]>([]);
const [isUpdatingOrder, setIsUpdatingOrder] = useState(false);
const [showQRMenu, setShowQRMenu] = useState(false);
const [isRegenerating, setIsRegenerating] = useState(false);
// Debounce search query
useEffect(() => {
@@ -249,6 +253,72 @@ const Orders: React.FC = () => {
}
};
const handlePrintQR = () => {
const canvas = document.querySelector('.qr-canvas canvas') as HTMLCanvasElement;
if (!canvas || !selectedOrder) return;
const qrDataURL = canvas.toDataURL('image/png');
const printWindow = window.open('', '_blank');
if (printWindow) {
printWindow.document.write(`
<html>
<head>
<title>Print QR Code - Order #${selectedOrder.displayOrderId}</title>
<style>
body { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: sans-serif; margin: 0; }
img { width: 300px; height: 300px; }
.order-info { margin-top: 40px; text-align: center; }
h1 { margin: 0; font-size: 24px; }
p { margin: 5px 0; color: #666; }
</style>
</head>
<body>
<img src="${qrDataURL}" />
<div class="order-info">
<h1>Order #${selectedOrder.displayOrderId}</h1>
<p>Scan to verify at terminal</p>
<p>Customer: ${selectedOrder.user?.name || 'Walk-in'}</p>
</div>
<script>
window.onload = () => {
window.print();
setTimeout(() => window.close(), 100);
};
</script>
</body>
</html>
`);
printWindow.document.close();
}
};
const handleRegenerateQR = async () => {
if (!selectedOrder) return;
setIsRegenerating(true);
try {
const newOrderNumber = `ORD-${Math.random().toString(36).substring(2, 10).toUpperCase()}`;
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/${selectedOrder.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...selectedOrder,
orderNumber: newOrderNumber
})
});
if (response.ok) {
fetchOrders(); // This will refresh the whole list and the selected order
setShowQRMenu(false);
} else {
alert('Failed to regenerate sync ID');
}
} catch (error) {
console.error('Error regenerating QR:', error);
} finally {
setIsRegenerating(false);
}
};
const filteredProducts = useMemo(() => {
if (!editSearchQuery.trim()) return [];
return allProducts.filter(p =>
@@ -583,6 +653,50 @@ const Orders: React.FC = () => {
{selectedOrder.items.reduce((sum, item) => sum + item.quantity, 0)}
</div>
</div>
{/* QR Verification Section */}
<div className="flex items-center gap-4 pl-8 border-l border-slate-200">
<div className="relative">
<div
onClick={() => setShowQRMenu(!showQRMenu)}
className="p-1.5 bg-white rounded-lg border border-slate-200 shadow-sm transition-all hover:scale-105 hover:border-indigo-300 cursor-pointer qr-canvas relative group"
>
<QRCodeCanvas
value={selectedOrder.orderNumber}
size={56}
level="H"
includeMargin={false}
/>
<div className="absolute inset-0 bg-indigo-600/0 group-hover:bg-indigo-600/5 transition-colors rounded-lg flex items-center justify-center">
<Plus size={16} className="text-indigo-600 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</div>
{showQRMenu && (
<div className="absolute bottom-full left-0 mb-4 w-64 bg-white border border-slate-200 rounded-2xl shadow-2xl z-20 overflow-hidden py-1 animate-in fade-in slide-in-from-bottom-2 duration-200">
<div className="px-4 py-2 text-[10px] font-bold text-slate-400 uppercase tracking-widest border-b border-slate-50">Sync Actions</div>
<button
onClick={handlePrintQR}
className="w-full text-left px-4 py-3 text-sm font-semibold text-slate-700 hover:bg-slate-50 flex items-center gap-3 transition-colors"
>
<Printer size={16} className="text-emerald-500" /> Print Sync Token
</button>
<button
onClick={handleRegenerateQR}
disabled={isRegenerating}
className="w-full text-left px-4 py-3 text-sm font-semibold text-slate-700 hover:bg-slate-50 flex items-center gap-3 transition-colors disabled:opacity-50"
>
<RefreshCw size={16} className={`text-indigo-500 ${isRegenerating ? 'animate-spin' : ''}`} />
{isRegenerating ? 'Regenerating...' : 'Regenerate Sync ID'}
</button>
</div>
)}
</div>
<div>
<div className="text-[9px] text-slate-400 uppercase font-black tracking-widest mb-0.5">Verify Order</div>
<div className="text-[10px] font-black text-slate-800 uppercase tracking-tighter">Scan for Terminal Sync</div>
</div>
</div>
</div>
<div className="text-right flex flex-col gap-2 min-w-[200px]">
<div className="flex justify-between items-center text-xs text-slate-500 font-bold">

View File

@@ -21,75 +21,91 @@ import {
} from 'recharts';
import { motion } from 'framer-motion';
const salesData = [
{ time: '12am', value: 0 },
{ time: '2am', value: 0 },
{ time: '4am', value: 0 },
{ time: '6am', value: 0 },
{ time: '8am', value: 5000 },
{ time: '10am', value: 16000 },
{ time: '12pm', value: 6000 },
];
const trendingItems = [
{ name: "GHEE ROAST", category: "BREAK FAST", qty: 64, image: "https://images.unsplash.com/photo-1668236543090-82eba5ee5976?w=200&q=80" },
{ name: "KAL DOSA (2pcs)", category: "BREAK FAST", qty: 60, image: "https://images.unsplash.com/photo-1589301760014-d929f3979dbc?w=200&q=80" },
{ name: "VEG S.W", category: "SNACKS", qty: 51, image: "https://images.unsplash.com/photo-1528735602780-2552fd46c7af?w=200&q=80" },
{ name: "PLAIN DOSA", category: "BREAK FAST", qty: 51, image: "https://images.unsplash.com/photo-1610192244261-3f33de3f55e4?w=200&q=80" }
];
const StoreDashboard = () => {
const [activeTab, setActiveTab] = useState('Sales');
const [timeRange, setTimeRange] = useState('Today');
const [stats, setStats] = useState({
totalSales: 48438,
activeOrders: 912,
dailyCustomers: 813,
revenueGrowth: 12.5
totalSales: 0,
activeOrders: 0,
dailyCustomers: 0,
revenueGrowth: 0
});
const [trendingItems, setTrendingItems] = useState<any[]>([]);
const [salesData, setSalesData] = useState<any[]>([]);
const [insights, setInsights] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const formatCurrency = (val: any) => {
const num = Number(val);
return isNaN(num) ? '0' : num.toLocaleString();
};
useEffect(() => {
const fetchStats = async () => {
try {
setIsLoading(true);
const response = await fetch('/api/dashboard/stats');
if (response.ok) {
const data = await response.json();
// Find RIT Canteen in storeOverview if available, otherwise use general stats
console.log('Dashboard data received successfully:', data);
// Set basic stats from general stats if available
if (data.stats) {
setStats({
totalSales: data.stats.totalSales,
activeOrders: data.stats.activeOrders,
dailyCustomers: data.stats.dailyCustomers,
revenueGrowth: data.stats.growth
});
}
// Override with RIT Canteen specific data if found in overview
if (data.storeOverview && data.storeOverview.length > 0) {
const ritStore = data.storeOverview.find((s: any) => s.name === 'RIT Canteen');
if (ritStore) {
setStats({
setStats(prev => ({
...prev,
totalSales: ritStore.sale,
activeOrders: ritStore.orders,
dailyCustomers: ritStore.orders * 0.9, // Approximation
revenueGrowth: 12.5
});
} else {
setStats(data.stats);
}));
}
} else {
setStats(data.stats);
}
// Set other dynamic data
if (data.trendingItems) setTrendingItems(data.trendingItems);
if (data.hourlySales) setSalesData(data.hourlySales);
if (data.insights) setInsights(data.insights);
}
} catch (error) {
console.error('Error fetching dashboard stats:', error);
} finally {
setIsLoading(false);
}
};
fetchStats();
}, []);
const pieData = [
{ name: 'Full Payment', value: stats.totalSales, color: '#8b5cf6' },
{ name: 'Full Payment', value: Number(stats.totalSales) || 0, color: '#8b5cf6' },
{ name: 'Credit', value: 0, color: '#fbbf24' }
];
const insights = [
{ text: `${stats.activeOrders} orders at RIT Canteen! Clearly the crowd's found their happy place 💃🕺`, color: "bg-rose-50 text-rose-600 border-rose-100" },
{ text: `${(stats.totalSales / (stats.activeOrders || 1)).toFixed(2)} average order value at RIT Canteen! Either everyone's hungry or just living large 🔥😋`, color: "bg-emerald-50 text-emerald-600 border-emerald-100" },
{ text: `RIT Canteen had ${stats.activeOrders} orders but only ${Math.round(stats.dailyCustomers)} customers — Maybe customers are shy and didn't give their names 🥰`, color: "bg-orange-50 text-orange-600 border-orange-100" },
{ text: `RIT Canteen clocked ₹${stats.totalSales.toLocaleString()} — ka-ching! That's called business booming 💸📈`, color: "bg-blue-50 text-blue-600 border-blue-100" },
{ text: "RIT Canteen reaches its morning sales peak around 9AM — breakfast rush in full swing 🍳🥞", color: "bg-indigo-50 text-indigo-600 border-indigo-100" }
];
if (isLoading) {
return (
<div className="h-screen flex items-center justify-center bg-slate-50/50">
<motion.div
animate={{ scale: [1, 1.1, 1], opacity: [0.5, 1, 0.5] }}
transition={{ repeat: Infinity, duration: 1.5 }}
className="text-[#0f4475] font-black uppercase tracking-widest text-sm"
>
Analyzing Store Data...
</motion.div>
</div>
);
}
return (
<div className="p-8 space-y-8 bg-slate-50/50 min-h-screen font-inter">
@@ -200,7 +216,7 @@ const StoreDashboard = () => {
</PieChart>
</ResponsiveContainer>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">{stats.totalSales.toLocaleString()}</h2>
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">{formatCurrency(stats.totalSales)}</h2>
</div>
<div className="flex gap-6 mt-4">
<div className="flex items-center gap-2">