Cart stock updation added

This commit is contained in:
Sidharth Prabhu
2026-04-18 21:13:34 +05:30
parent 8a41d650a2
commit c36b4e4de0
12 changed files with 233 additions and 88 deletions

View File

@@ -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 />} />

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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 (

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -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>
)}

View File

@@ -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
}
});