Cart stock updation added
This commit is contained in:
@@ -14,6 +14,7 @@ import LoginScreen from './pages/LoginScreen';
|
||||
import ProfileScreen from './pages/ProfileScreen';
|
||||
import ChangePinScreen from './pages/ChangePinScreen';
|
||||
import StallDetailScreen from './pages/StallDetailScreen';
|
||||
import StockAlert from './components/StockAlert';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
@@ -21,6 +22,7 @@ function App() {
|
||||
<AuthProvider>
|
||||
<FoodProvider>
|
||||
<CartProvider>
|
||||
<StockAlert />
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginScreen />} />
|
||||
|
||||
@@ -214,3 +214,24 @@
|
||||
min-width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.limit-badge {
|
||||
background-color: #fef2f2;
|
||||
color: #ef4444;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
padding: 1px 6px;
|
||||
border-radius: 20px;
|
||||
white-space: nowrap;
|
||||
border: 1px solid #fee2e2;
|
||||
}
|
||||
|
||||
.at-limit button:last-child {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
|
||||
.item-card.limit-reached {
|
||||
border-left: 3px solid #ef4444;
|
||||
}
|
||||
|
||||
@@ -13,10 +13,11 @@ const ItemCard: React.FC<ItemCardProps> = ({ item, isLast }) => {
|
||||
const navigate = useNavigate();
|
||||
const { addToCart, updateQuantity, getItemQuantity } = useCart();
|
||||
const quantity = getItemQuantity(item.id);
|
||||
const isLimitReached = item.stock !== undefined && quantity >= item.stock && item.stock > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`item-card ${isLast ? 'last' : ''} ${item.stock === 0 ? 'out-of-stock' : ''}`}
|
||||
className={`item-card ${isLast ? 'last' : ''} ${item.stock === 0 ? 'out-of-stock' : ''} ${isLimitReached ? 'limit-reached' : ''}`}
|
||||
onClick={() => navigate(`/item/${item.id}`)}
|
||||
>
|
||||
<div className="item-info">
|
||||
@@ -26,6 +27,7 @@ const ItemCard: React.FC<ItemCardProps> = ({ item, isLast }) => {
|
||||
</div>
|
||||
{item.isPopular && <span className="bestseller-badge">Popular</span>}
|
||||
{item.stock === 0 && <span className="out-of-stock-badge">Out of Stock</span>}
|
||||
{isLimitReached && <span className="limit-badge">Only {item.stock} left</span>}
|
||||
</div>
|
||||
|
||||
<h3 className="item-name">{item.name}</h3>
|
||||
@@ -53,10 +55,13 @@ const ItemCard: React.FC<ItemCardProps> = ({ item, isLast }) => {
|
||||
{item.stock === 0 ? 'SOLD OUT' : 'ADD'}
|
||||
</button>
|
||||
) : (
|
||||
<div className="quantity-controls">
|
||||
<div className={`quantity-controls ${isLimitReached ? 'at-limit' : ''}`}>
|
||||
<button onClick={() => updateQuantity(item.id, -1)}>−</button>
|
||||
<span className="quantity">{quantity}</span>
|
||||
<button onClick={() => addToCart(item)}>+</button>
|
||||
<button
|
||||
onClick={() => addToCart(item)}
|
||||
className={isLimitReached ? 'disabled' : ''}
|
||||
>+</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,8 @@ interface CartContextType {
|
||||
getItemQuantity: (itemId: string) => number;
|
||||
totalItems: number;
|
||||
totalPrice: number;
|
||||
stockError: string | null;
|
||||
clearStockError: () => void;
|
||||
}
|
||||
|
||||
const CartContext = createContext<CartContextType | undefined>(undefined);
|
||||
@@ -19,14 +21,25 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const savedCart = localStorage.getItem('cart');
|
||||
return savedCart ? JSON.parse(savedCart) : [];
|
||||
});
|
||||
const [stockError, setStockError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('cart', JSON.stringify(cart));
|
||||
}, [cart]);
|
||||
|
||||
const clearStockError = () => setStockError(null);
|
||||
|
||||
const addToCart = (item: FoodItem) => {
|
||||
setCart((prevCart) => {
|
||||
const existingIndex = prevCart.findIndex((i) => i.id === item.id);
|
||||
|
||||
// Stock check
|
||||
const currentQty = existingIndex !== -1 ? prevCart[existingIndex].quantity : 0;
|
||||
if (item.stock !== undefined && currentQty >= item.stock) {
|
||||
setStockError(`Only ${item.stock} left for ${item.name}`);
|
||||
return prevCart;
|
||||
}
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
const newCart = [...prevCart];
|
||||
newCart[existingIndex] = {
|
||||
@@ -47,9 +60,17 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
setCart((prevCart) => {
|
||||
const index = prevCart.findIndex((item) => item.id === itemId);
|
||||
if (index !== -1) {
|
||||
const newCart = [...prevCart];
|
||||
const newQty = newCart[index].quantity + delta;
|
||||
const item = prevCart[index];
|
||||
const newQty = item.quantity + delta;
|
||||
|
||||
// Stock check for increment
|
||||
if (delta > 0 && item.stock !== undefined && item.quantity >= item.stock) {
|
||||
setStockError(`Only ${item.stock} left for ${item.name}`);
|
||||
return prevCart;
|
||||
}
|
||||
|
||||
if (newQty > 0) {
|
||||
const newCart = [...prevCart];
|
||||
newCart[index] = { ...newCart[index], quantity: newQty };
|
||||
return newCart;
|
||||
} else {
|
||||
@@ -84,6 +105,8 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
getItemQuantity,
|
||||
totalItems,
|
||||
totalPrice,
|
||||
stockError,
|
||||
clearStockError,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -24,8 +24,8 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
const fetchData = async (silent = false) => {
|
||||
if (!silent) setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [baseItemsRes, productsRes, stallsRes] = await Promise.all([
|
||||
@@ -119,15 +119,22 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
setFoodItems(mappedFoodItems);
|
||||
setStalls(mappedStalls);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
if (!silent) setError(err.message);
|
||||
console.error('Error fetching food data:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
if (!silent) setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
|
||||
// Silent background polling every 10 seconds to sync stock
|
||||
const pollInterval = setInterval(() => {
|
||||
fetchData(true);
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(pollInterval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Smartphone } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import Header from '../components/Header';
|
||||
import StockConflictModal from '../components/StockConflictModal';
|
||||
import { useCart } from '../contexts/CartContext';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useFood } from '../contexts/FoodContext';
|
||||
@@ -17,11 +19,15 @@ const UPI_APPS = [
|
||||
|
||||
const CheckoutScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { cart, totalPrice, clearCart } = useCart();
|
||||
const { cart, totalPrice, clearCart, removeFromCart, updateQuantity } = useCart();
|
||||
const { user } = useAuth();
|
||||
const { refreshData } = useFood();
|
||||
const [selectedApp, setSelectedApp] = useState('gpay');
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
// Conflict state
|
||||
const [stockConflicts, setStockConflicts] = useState<any[]>([]);
|
||||
const [showConflictModal, setShowConflictModal] = useState(false);
|
||||
|
||||
const handlePlaceOrder = async () => {
|
||||
if (!user) return;
|
||||
@@ -41,10 +47,6 @@ const CheckoutScreen: React.FC = () => {
|
||||
stallName: item.stallName || null
|
||||
}))
|
||||
};
|
||||
|
||||
console.warn('❗ DIAGNOSTIC: PRE-ORDER PAYLOAD CHECK');
|
||||
console.table(orderData.items.map(i => ({ Name: i.productName, StallID: i.stallId, StallName: i.stallName })));
|
||||
console.log('Full Payload:', JSON.stringify(orderData, null, 2));
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/orders`, {
|
||||
@@ -53,26 +55,23 @@ const CheckoutScreen: React.FC = () => {
|
||||
body: JSON.stringify(orderData),
|
||||
});
|
||||
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (contentType && contentType.indexOf("application/json") !== -1) {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await refreshData();
|
||||
clearCart();
|
||||
navigate('/success', {
|
||||
state: {
|
||||
orderNumber: data.orderNumber,
|
||||
displayOrderId: data.displayOrderId
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('Order Logic Error:', data.message || data);
|
||||
alert(data.message || 'Failed to place order');
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await refreshData();
|
||||
clearCart();
|
||||
navigate('/success', {
|
||||
state: {
|
||||
orderNumber: data.orderNumber,
|
||||
displayOrderId: data.displayOrderId
|
||||
}
|
||||
});
|
||||
} else if (data.errorType === 'STOCK_ERROR') {
|
||||
console.error('Final Step Stock Conflict:', data.conflicts);
|
||||
setStockConflicts(data.conflicts || []);
|
||||
setShowConflictModal(true);
|
||||
await refreshData(true); // Sync background stock
|
||||
} else {
|
||||
const text = await response.text();
|
||||
console.error('Order Server Error (Non-JSON):', text);
|
||||
alert('Server Error. Check console for details.');
|
||||
alert(data.message || 'Failed to place order');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Order error:', error);
|
||||
@@ -82,6 +81,24 @@ const CheckoutScreen: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveConflictItem = (productId: number) => {
|
||||
removeFromCart(productId.toString());
|
||||
const remaining = stockConflicts.filter(c => c.productId !== productId);
|
||||
setStockConflicts(remaining);
|
||||
if (remaining.length === 0) setShowConflictModal(false);
|
||||
};
|
||||
|
||||
const handleAdjustConflictQuantity = (productId: number, newQty: number) => {
|
||||
const item = cart.find(i => i.id === productId.toString());
|
||||
if (item) {
|
||||
const delta = newQty - item.quantity;
|
||||
updateQuantity(productId.toString(), delta);
|
||||
}
|
||||
const remaining = stockConflicts.filter(c => c.productId !== productId);
|
||||
setStockConflicts(remaining);
|
||||
if (remaining.length === 0) setShowConflictModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container checkout-page">
|
||||
<Header title="Checkout" showCart={false} />
|
||||
@@ -141,6 +158,17 @@ const CheckoutScreen: React.FC = () => {
|
||||
{isProcessing ? 'Processing...' : `Pay ₹${totalPrice.toFixed(2)} & Place Order`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{showConflictModal && (
|
||||
<StockConflictModal
|
||||
conflicts={stockConflicts}
|
||||
onRemoveItem={handleRemoveConflictItem}
|
||||
onAdjustQuantity={handleAdjustConflictQuantity}
|
||||
onClose={() => navigate('/cart')}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -259,3 +259,22 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.limit-reached-info {
|
||||
background-color: #fef2f2;
|
||||
border: 1px solid #fee2e2;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #ef4444;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.footer-quantity-controls button:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import Header from '../components/Header';
|
||||
import { useCart } from '../contexts/CartContext';
|
||||
import { useFood } from '../contexts/FoodContext';
|
||||
@@ -13,6 +14,7 @@ const ItemDetailScreen: React.FC = () => {
|
||||
|
||||
const item = foodItems.find((i) => i.id === itemId);
|
||||
const quantity = item ? getItemQuantity(item.id) : 0;
|
||||
const isLimitReached = item && item.stock !== undefined && quantity >= item.stock && item.stock > 0;
|
||||
|
||||
if (isLoading && foodItems.length === 0) {
|
||||
return <div className="container" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Loading...</div>;
|
||||
@@ -69,10 +71,17 @@ const ItemDetailScreen: React.FC = () => {
|
||||
<div className="info-row">
|
||||
<span className="info-label">Availability</span>
|
||||
<span className={`info-value ${item.stock && item.stock > 0 ? 'green' : 'red'}`}>
|
||||
{item.stock && item.stock > 0 ? 'In Stock' : 'Out of Stock'}
|
||||
{item.stock && item.stock > 0 ? `In Stock (${item.stock} left)` : 'Out of Stock'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLimitReached && (
|
||||
<div className="limit-reached-info">
|
||||
<AlertCircle size={18} />
|
||||
<span>You have reached the maximum available quantity ({item.stock}) for this item.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -98,7 +107,7 @@ const ItemDetailScreen: React.FC = () => {
|
||||
<span className="quantity">{quantity}</span>
|
||||
<button
|
||||
onClick={() => addToCart(item)}
|
||||
disabled={item.stock !== undefined && quantity >= item.stock}
|
||||
disabled={isLimitReached}
|
||||
>+</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -65,21 +65,21 @@ const MyOrdersScreen: React.FC = () => {
|
||||
const available: FoodItem[] = [];
|
||||
|
||||
order.items.forEach(orderItem => {
|
||||
// Find the current food item by name (since orders store names)
|
||||
// Robustness: matching by name is fallback if IDs aren't directly available in OrderItem DTO
|
||||
const currentItem = foodItems.find(fi => fi.name === orderItem.productName);
|
||||
|
||||
if (currentItem) {
|
||||
if (currentItem.stock && currentItem.stock > 0) {
|
||||
// Add multiple times based on original quantity
|
||||
for (let i = 0; i < orderItem.quantity; i++) {
|
||||
available.push(currentItem);
|
||||
}
|
||||
} else {
|
||||
missing.push(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 in catalog
|
||||
missing.push(orderItem.productName); // Item no longer exists or out of stock
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user