Files
Canteen-Management-System/ordering_site/src/pages/MyOrdersScreen.tsx
Sidharth Prabhu f915a11eb8 Refund added
2026-06-24 14:06:31 +05:30

459 lines
18 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Clock, ShoppingBag as ShoppingBagIcon, ChevronRight as ChevronRightIcon, X, RefreshCcw, AlertCircle } from 'lucide-react';
import { QRCodeCanvas } from 'qrcode.react';
import { motion, AnimatePresence } from 'framer-motion';
import Header from '../components/Header';
import BottomNav from '../components/BottomNav';
import { useAuth } from '../contexts/AuthContext';
import { useFood } from '../contexts/FoodContext';
import { useCart } from '../contexts/CartContext';
import type { FoodItem } from '../types';
import './MyOrdersScreen.css';
interface Order {
id: number;
orderNumber: string;
displayOrderId: string;
totalAmount: number;
status: string;
paymentMethod: string;
createdAt: string;
isArchived: boolean;
items: Array<{
productName: string;
quantity: number;
price: number;
}>;
}
const MyOrdersScreen: React.FC = () => {
const navigate = useNavigate();
const { user } = useAuth();
const { foodItems } = useFood();
const { addToCart, clearCart } = useCart();
const [orders, setOrders] = useState<Order[]>([]);
const [loading, setLoading] = useState(true);
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
// State for stock alert
const [showStockAlert, setShowStockAlert] = useState(false);
const [unavailableItems, setUnavailableItems] = useState<string[]>([]);
const [pendingItems, setPendingItems] = useState<FoodItem[]>([]);
const [isCancelling, setIsCancelling] = useState(false);
const isOrderExpired = (order: Order) => {
if (order.isArchived) return true;
const orderDate = new Date(order.createdAt).toDateString();
const today = new Date().toDateString();
return orderDate !== today;
};
useEffect(() => {
if (user?.id) {
fetchOrders();
}
}, [user]);
// Polling for live updates (Sync when Dashboard regenerates QR or changes status)
useEffect(() => {
let interval: ReturnType<typeof setInterval>;
// Only poll if there are active (non-finalized) orders
const hasActiveOrders = orders.some(o =>
o.status.toUpperCase() !== 'COMPLETED' &&
o.status.toUpperCase() !== 'CANCELLED'
);
if (user?.id && hasActiveOrders) {
interval = setInterval(() => {
fetchOrders();
}, 10000); // Poll every 10 seconds
}
return () => {
if (interval) clearInterval(interval);
};
}, [user?.id, orders]);
const fetchOrders = async () => {
try {
const token = localStorage.getItem('token');
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/user/${user?.id}`, {
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
const data = await response.json();
setOrders(data);
} catch (error) {
console.error('Error fetching orders:', error);
} finally {
setLoading(false);
}
};
const handleRepeatOrder = (order: Order) => {
const missing: string[] = [];
const available: FoodItem[] = [];
order.items.forEach(orderItem => {
const currentItem = foodItems.find(fi => fi.name === orderItem.productName);
if (currentItem && currentItem.stock !== undefined && currentItem.stock > 0) {
// Only add up to available stock
const canAdd = Math.min(orderItem.quantity, currentItem.stock);
for (let i = 0; i < canAdd; i++) {
available.push(currentItem);
}
if (canAdd < orderItem.quantity) {
missing.push(`${orderItem.productName} (only ${canAdd} of ${orderItem.quantity} available)`);
}
} else {
missing.push(orderItem.productName); // Item no longer exists or out of stock
}
});
if (missing.length > 0) {
setUnavailableItems(missing);
setPendingItems(available);
setShowStockAlert(true);
} else {
// All items available
performRepeat(available);
}
};
const handleCancelOrder = async (orderId: number) => {
if (!window.confirm('Are you sure you want to cancel this order?')) return;
setIsCancelling(true);
try {
const token = localStorage.getItem('token');
const response = await fetch(`http://${window.location.hostname}:8080/api/orders/${orderId}/cancel`, {
method: 'POST',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
const data = await response.json();
if (data.success) {
fetchOrders();
if (selectedOrder?.id === orderId) {
setSelectedOrder({ ...selectedOrder, status: 'CANCELLED' });
}
} else {
alert(data.message || 'Failed to cancel order');
}
} catch (error) {
console.error('Error cancelling order:', error);
alert('Error cancelling order');
} finally {
setIsCancelling(false);
}
};
const performRepeat = (items: FoodItem[]) => {
clearCart();
items.forEach(item => addToCart(item));
navigate('/cart');
};
const handleProceedPartial = () => {
performRepeat(pendingItems);
setShowStockAlert(false);
};
const latestOrder = orders.length > 0 ? orders[0] : null;
const previousOrders = orders.length > 1 ? orders.slice(1) : [];
if (loading) {
return <div className="container">Loading your orders...</div>;
}
return (
<div className="container orders-page">
<Header title="My Orders" showCart={false} />
<main className="orders-content safe-area-bottom">
{orders.length === 0 ? (
<div className="empty-orders">
<ShoppingBagIcon size={64} className="empty-icon" />
<h3>No orders yet</h3>
<p>Hungry? Place your first order now!</p>
</div>
) : (
<>
{/* Latest Order Section */}
{latestOrder && (
<section className="latest-order-section">
<div className="section-label">Active/Latest Order</div>
<div className="latest-order-card" onClick={() => setSelectedOrder(latestOrder)}>
<div className="latest-order-info">
<div className="order-main-info">
<span className="order-number">Order #{latestOrder.displayOrderId || latestOrder.id}</span>
<span className="order-date">{new Date(latestOrder.createdAt).toLocaleDateString()}</span>
</div>
<div className="order-status-badge">{latestOrder.status}</div>
</div>
<div className={`latest-qr-wrapper ${latestOrder.status.toUpperCase() === 'COMPLETED' ? 'qr-completed' : ''} ${latestOrder.status.toUpperCase() === 'CANCELLED' ? 'qr-cancelled' : ''} ${isOrderExpired(latestOrder) ? 'qr-expired' : ''}`}>
<div className="qr-container">
<QRCodeCanvas value={latestOrder.orderNumber} size={120} className="mini-qr" />
{latestOrder.status.toUpperCase() === 'COMPLETED' && (
<div className="qr-overlay mini">
<span className="overlay-text mini">COMPLETED</span>
</div>
)}
{latestOrder.status.toUpperCase() === 'CANCELLED' && (
<div className="qr-overlay mini">
<span className="overlay-text mini cancelled">CANCELLED</span>
</div>
)}
{isOrderExpired(latestOrder) && (
<div className="qr-overlay mini">
<span className="overlay-text mini expired">EXPIRED</span>
</div>
)}
</div>
<div className="qr-hint-text">
{isOrderExpired(latestOrder) ? 'QR Expired' : (latestOrder.status.toUpperCase() === 'COMPLETED' ? 'Order Fulfilled' : latestOrder.status.toUpperCase() === 'CANCELLED' ? 'Order Cancelled' : 'Tap to enlarge QR')}
</div>
</div>
<div className="order-items-summary">
{latestOrder.items.map((item, idx) => (
<span key={idx}>{item.quantity}x {item.productName}{idx < latestOrder.items.length - 1 ? ', ' : ''}</span>
))}
</div>
<div className="order-total-bar">
<span>Total Amount</span>
<span className="amount-text">R{latestOrder.totalAmount.toFixed(2)}</span>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
className="repeat-order-btn mini"
style={{ flex: 1 }}
onClick={(e) => {
e.stopPropagation();
handleRepeatOrder(latestOrder);
}}
>
<RefreshCcw size={16} /> Repeat
</button>
{latestOrder.status.toUpperCase() !== 'COMPLETED' && latestOrder.status.toUpperCase() !== 'CANCELLED' && !isOrderExpired(latestOrder) && (
<button
className="repeat-order-btn mini"
style={{ flex: 1, backgroundColor: '#fee2e2', color: '#dc2626' }}
disabled={isCancelling}
onClick={(e) => {
e.stopPropagation();
handleCancelOrder(latestOrder.id);
}}
>
<X size={16} /> Cancel
</button>
)}
</div>
</div>
</section>
)}
{/* Previous Orders List */}
{previousOrders.length > 0 && (
<section className="previous-orders-section">
<div className="section-label">Previous Orders</div>
<div className="orders-list">
{previousOrders.map((order) => (
<div key={order.id} className="order-item-list" onClick={() => setSelectedOrder(order)}>
<div className="order-icon-bg">
<Clock size={20} />
</div>
<div className="order-list-info">
<div className="order-list-top">
<span className="order-list-number">#{order.displayOrderId}</span>
<span className="order-list-price">R{order.totalAmount.toFixed(0)}</span>
</div>
<div className="order-list-bottom">
<span className="order-list-date">{new Date(order.createdAt).toLocaleDateString()}</span>
<span className="order-list-items">{order.items.length} items</span>
</div>
</div>
<ChevronRightIcon size={20} className="list-chevron" />
</div>
))}
</div>
</section>
)}
</>
)}
</main>
{/* Order Detail Modal / Overlay */}
<AnimatePresence>
{selectedOrder && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="order-detail-overlay"
onClick={() => setSelectedOrder(null)}
>
<motion.div
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className="order-detail-modal"
onClick={(e) => e.stopPropagation()}
>
<button className="close-details" onClick={() => setSelectedOrder(null)}>
<X size={24} />
</button>
<div className="modal-header">
<h3>Order Details</h3>
<p className="modal-order-id">Order #{selectedOrder.displayOrderId}</p>
</div>
<div className={`modal-qr-section ${selectedOrder.status.toUpperCase() === 'COMPLETED' ? 'qr-completed' : ''} ${selectedOrder.status.toUpperCase() === 'CANCELLED' ? 'qr-cancelled' : ''} ${isOrderExpired(selectedOrder) ? 'qr-expired' : ''}`}>
<div className="qr-container">
<QRCodeCanvas value={selectedOrder.orderNumber} size={200} includeMargin={true} />
{selectedOrder.status.toUpperCase() === 'COMPLETED' && (
<div className="qr-overlay">
<span className="overlay-text">COMPLETED</span>
</div>
)}
{selectedOrder.status.toUpperCase() === 'CANCELLED' && (
<div className="qr-overlay">
<span className="overlay-text cancelled">CANCELLED</span>
</div>
)}
{isOrderExpired(selectedOrder) && (
<div className="qr-overlay">
<span className="overlay-text expired">EXPIRED</span>
</div>
)}
</div>
<p className="modal-qr-hint">
{isOrderExpired(selectedOrder)
? 'QR Expired'
: selectedOrder.status.toUpperCase() === 'COMPLETED'
? 'This order has been fulfilled'
: selectedOrder.status.toUpperCase() === 'CANCELLED'
? 'This order has been cancelled'
: 'Show this QR code at the counter'}
</p>
</div>
<div className="modal-info-list">
<div className="info-item">
<span className="label">Date</span>
<span className="value">{new Date(selectedOrder.createdAt).toLocaleString()}</span>
</div>
<div className="info-item">
<span className="label">Payment</span>
<span className="value">{selectedOrder.paymentMethod}</span>
</div>
<div className="info-item">
<span className="label">Status</span>
<span className={`value status-badge ${selectedOrder.status.toLowerCase()}`}>
{selectedOrder.status}
</span>
</div>
</div>
<div className="modal-items-section">
<h4>Items Ordered</h4>
{selectedOrder.items.map((item, idx) => (
<div key={idx} className="modal-item-row">
<span>{item.quantity} x {item.productName}</span>
<span>R{(item.price * item.quantity).toFixed(0)}</span>
</div>
))}
<div className="modal-total-row">
<span>Grand Total</span>
<span>R{selectedOrder.totalAmount.toFixed(2)}</span>
</div>
</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
<button className="repeat-order-btn" style={{ flex: 1, marginTop: 0 }} onClick={() => handleRepeatOrder(selectedOrder)}>
<RefreshCcw size={18} /> Repeat
</button>
{selectedOrder.status.toUpperCase() !== 'COMPLETED' && selectedOrder.status.toUpperCase() !== 'CANCELLED' && !isOrderExpired(selectedOrder) && (
<button
className="repeat-order-btn"
style={{ flex: 1, marginTop: 0, backgroundColor: '#fee2e2', color: '#dc2626' }}
disabled={isCancelling}
onClick={() => handleCancelOrder(selectedOrder.id)}
>
<X size={18} /> Cancel
</button>
)}
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* Stock Alert Dialog */}
<AnimatePresence>
{showStockAlert && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="stock-alert-overlay"
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="stock-alert-modal"
>
<div className="alert-icon-wrapper">
<AlertCircle size={32} />
</div>
<h3>Items Unavailable</h3>
<p>Some items from your previous order are currently out of stock or unavailable.</p>
<div className="unavailable-items-list">
{unavailableItems.map((item, idx) => (
<div key={idx} className="unavailable-item-chip">
<X size={14} /> {item}
</div>
))}
</div>
<div className="alert-actions">
{pendingItems.length > 0 ? (
<button className="btn-proceed" onClick={handleProceedPartial}>
Proceed without these items
</button>
) : (
<div style={{ marginBottom: '12px', fontSize: '13px', color: '#e11d48', fontWeight: '600' }}>
All items in this order are currently unavailable.
</div>
)}
<button className="btn-discard" onClick={() => setShowStockAlert(false)}>
Discard
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
<BottomNav />
</div>
);
};
export default MyOrdersScreen;