Cart stock updation added
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,3 @@
|
|||||||
# ordering-site/*
|
ordering-site/*
|
||||||
# ordering_site/
|
ordering_site/
|
||||||
counter-frontend/
|
counter-frontend/
|
||||||
@@ -34,6 +34,9 @@ public class OrderController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private com.rit.canteen.sales.service.OrderArchiverService orderArchiverService;
|
private com.rit.canteen.sales.service.OrderArchiverService orderArchiverService;
|
||||||
|
|
||||||
|
// Use ThreadLocal to safely store conflicts for the current request context
|
||||||
|
private static final ThreadLocal<List<Map<String, Object>>> requestConflicts = new ThreadLocal<>();
|
||||||
|
|
||||||
@GetMapping("/all")
|
@GetMapping("/all")
|
||||||
public ResponseEntity<?> getAllOrders(
|
public ResponseEntity<?> getAllOrders(
|
||||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
|
||||||
@@ -113,54 +116,79 @@ public class OrderController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<Map<String, Object>> placeOrder(@RequestBody Order order) {
|
@org.springframework.transaction.annotation.Transactional
|
||||||
// Link items to the order for bidirectional relationship
|
public ResponseEntity<?> placeOrder(@RequestBody Order order) {
|
||||||
if (order.getItems() != null) {
|
// 1. Pre-validation and linking
|
||||||
for (OrderItem item : order.getItems()) {
|
if (order.getItems() == null || order.getItems().isEmpty()) {
|
||||||
System.out.println("🛒 RECEIVED ITEM: " + item.getProductName() + " | StallID: " + item.getStallId() + " | StallName: " + item.getStallName());
|
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "Order must have items"));
|
||||||
item.setOrder(order);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Atomic Stock Check & Update
|
||||||
|
List<Map<String, Object>> stockConflicts = new ArrayList<>();
|
||||||
|
requestConflicts.remove(); // Clear before use
|
||||||
|
|
||||||
// Use the actual creation time or current time for counting
|
for (OrderItem item : order.getItems()) {
|
||||||
LocalDateTime now = LocalDateTime.now();
|
Long productId = item.getProductId();
|
||||||
order.setCreatedAt(now);
|
if (productId != null) {
|
||||||
|
int updatedRows = productRepository.decrementStock(productId, item.getQuantity());
|
||||||
// Calculate start of current day to find how many orders placed today
|
|
||||||
LocalDateTime startOfDay = now.toLocalDate().atStartOfDay();
|
if (updatedRows == 0) {
|
||||||
long todaysOrderCount = orderRepository.countByCreatedAtGreaterThanEqual(startOfDay);
|
com.rit.canteen.sales.model.Product p = productRepository.findById(productId).orElse(null);
|
||||||
|
int left = (p != null && p.getStock() != null) ? p.getStock() : 0;
|
||||||
// Generate formatted display ID (#001, #002...) resetting daily
|
|
||||||
String displayId = String.format("%03d", todaysOrderCount + 1);
|
Map<String, Object> conflict = new HashMap<>();
|
||||||
order.setDisplayOrderId(displayId);
|
conflict.put("productId", productId);
|
||||||
|
conflict.put("productName", item.getProductName());
|
||||||
// Save the complete order first
|
conflict.put("requested", item.getQuantity());
|
||||||
Order savedOrder = orderRepository.save(order);
|
conflict.put("available", left);
|
||||||
|
stockConflicts.add(conflict);
|
||||||
// --- Reduct Stock Logic ---
|
|
||||||
if (savedOrder.getItems() != null) {
|
|
||||||
for (OrderItem item : savedOrder.getItems()) {
|
|
||||||
Long productId = item.getProductId();
|
|
||||||
if (productId != null) {
|
|
||||||
productRepository.findById(productId).ifPresent(product -> {
|
|
||||||
int currentStock = product.getStock() != null ? product.getStock() : 0;
|
|
||||||
product.setStock(currentStock - item.getQuantity());
|
|
||||||
productRepository.save(product);
|
|
||||||
System.out.println("Updating Stock for " + product.getName() + ": " + currentStock + " -> " + product.getStock());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!stockConflicts.isEmpty()) {
|
||||||
|
requestConflicts.set(stockConflicts);
|
||||||
|
throw new RuntimeException("CONCURRENCY_STOCK_FAILURE");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Complete Order Details
|
||||||
|
for (OrderItem item : order.getItems()) {
|
||||||
|
item.setOrder(order);
|
||||||
|
}
|
||||||
|
|
||||||
System.out.println("Placed Daily Order: " + savedOrder.getId() + " -> Display ID: #" + displayId);
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
order.setCreatedAt(now);
|
||||||
|
LocalDateTime startOfDay = now.toLocalDate().atStartOfDay();
|
||||||
|
long todaysOrderCount = orderRepository.countByCreatedAtGreaterThanEqual(startOfDay);
|
||||||
|
String displayId = String.format("%03d", todaysOrderCount + 1);
|
||||||
|
order.setDisplayOrderId(displayId);
|
||||||
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
// 4. Final Save
|
||||||
response.put("success", true);
|
Order savedOrder = orderRepository.save(order);
|
||||||
response.put("orderNumber", savedOrder.getOrderNumber()); // Secure ID for QR
|
|
||||||
response.put("displayOrderId", savedOrder.getDisplayOrderId()); // Sequential ID (#001)
|
|
||||||
response.put("message", "Order placed successfully");
|
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
System.out.println("Placed Order: " + savedOrder.getId() + " -> Display ID: #" + displayId);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"success", true,
|
||||||
|
"orderNumber", savedOrder.getOrderNumber(),
|
||||||
|
"displayOrderId", savedOrder.getDisplayOrderId(),
|
||||||
|
"message", "Order placed successfully"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(RuntimeException.class)
|
||||||
|
public ResponseEntity<?> handleRuntimeException(RuntimeException e) {
|
||||||
|
if ("CONCURRENCY_STOCK_FAILURE".equals(e.getMessage())) {
|
||||||
|
List<Map<String, Object>> conflicts = requestConflicts.get();
|
||||||
|
requestConflicts.remove();
|
||||||
|
return ResponseEntity.status(400).body(Map.of(
|
||||||
|
"success", false,
|
||||||
|
"errorType", "STOCK_ERROR",
|
||||||
|
"message", "Some items in your cart are no longer available in the requested quantity.",
|
||||||
|
"conflicts", conflicts != null ? conflicts : new ArrayList<>()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return ResponseEntity.status(500).body(Map.of("success", false, "message", e.getMessage() != null ? e.getMessage() : "Internal Server Error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/user/{userId}")
|
@GetMapping("/user/{userId}")
|
||||||
|
|||||||
@@ -24,4 +24,7 @@ public interface ProductRepository extends JpaRepository<Product, Long> {
|
|||||||
List<String> findDistinctCategories();
|
List<String> findDistinctCategories();
|
||||||
|
|
||||||
boolean existsByNameAndCategory(String name, String category);
|
boolean existsByNameAndCategory(String name, String category);
|
||||||
|
@org.springframework.data.jpa.repository.Modifying
|
||||||
|
@org.springframework.data.jpa.repository.Query("UPDATE Product p SET p.stock = p.stock - :quantity WHERE p.id = :id AND p.stock >= :quantity")
|
||||||
|
int decrementStock(@org.springframework.data.repository.query.Param("id") Long id, @org.springframework.data.repository.query.Param("quantity") int quantity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import LoginScreen from './pages/LoginScreen';
|
|||||||
import ProfileScreen from './pages/ProfileScreen';
|
import ProfileScreen from './pages/ProfileScreen';
|
||||||
import ChangePinScreen from './pages/ChangePinScreen';
|
import ChangePinScreen from './pages/ChangePinScreen';
|
||||||
import StallDetailScreen from './pages/StallDetailScreen';
|
import StallDetailScreen from './pages/StallDetailScreen';
|
||||||
|
import StockAlert from './components/StockAlert';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -21,6 +22,7 @@ function App() {
|
|||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<FoodProvider>
|
<FoodProvider>
|
||||||
<CartProvider>
|
<CartProvider>
|
||||||
|
<StockAlert />
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginScreen />} />
|
<Route path="/login" element={<LoginScreen />} />
|
||||||
|
|||||||
@@ -214,3 +214,24 @@
|
|||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
text-align: center;
|
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 navigate = useNavigate();
|
||||||
const { addToCart, updateQuantity, getItemQuantity } = useCart();
|
const { addToCart, updateQuantity, getItemQuantity } = useCart();
|
||||||
const quantity = getItemQuantity(item.id);
|
const quantity = getItemQuantity(item.id);
|
||||||
|
const isLimitReached = item.stock !== undefined && quantity >= item.stock && item.stock > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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}`)}
|
onClick={() => navigate(`/item/${item.id}`)}
|
||||||
>
|
>
|
||||||
<div className="item-info">
|
<div className="item-info">
|
||||||
@@ -26,6 +27,7 @@ const ItemCard: React.FC<ItemCardProps> = ({ item, isLast }) => {
|
|||||||
</div>
|
</div>
|
||||||
{item.isPopular && <span className="bestseller-badge">Popular</span>}
|
{item.isPopular && <span className="bestseller-badge">Popular</span>}
|
||||||
{item.stock === 0 && <span className="out-of-stock-badge">Out of Stock</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>
|
</div>
|
||||||
|
|
||||||
<h3 className="item-name">{item.name}</h3>
|
<h3 className="item-name">{item.name}</h3>
|
||||||
@@ -53,10 +55,13 @@ const ItemCard: React.FC<ItemCardProps> = ({ item, isLast }) => {
|
|||||||
{item.stock === 0 ? 'SOLD OUT' : 'ADD'}
|
{item.stock === 0 ? 'SOLD OUT' : 'ADD'}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div className="quantity-controls">
|
<div className={`quantity-controls ${isLimitReached ? 'at-limit' : ''}`}>
|
||||||
<button onClick={() => updateQuantity(item.id, -1)}>−</button>
|
<button onClick={() => updateQuantity(item.id, -1)}>−</button>
|
||||||
<span className="quantity">{quantity}</span>
|
<span className="quantity">{quantity}</span>
|
||||||
<button onClick={() => addToCart(item)}>+</button>
|
<button
|
||||||
|
onClick={() => addToCart(item)}
|
||||||
|
className={isLimitReached ? 'disabled' : ''}
|
||||||
|
>+</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ interface CartContextType {
|
|||||||
getItemQuantity: (itemId: string) => number;
|
getItemQuantity: (itemId: string) => number;
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
totalPrice: number;
|
totalPrice: number;
|
||||||
|
stockError: string | null;
|
||||||
|
clearStockError: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CartContext = createContext<CartContextType | undefined>(undefined);
|
const CartContext = createContext<CartContextType | undefined>(undefined);
|
||||||
@@ -19,14 +21,25 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
const savedCart = localStorage.getItem('cart');
|
const savedCart = localStorage.getItem('cart');
|
||||||
return savedCart ? JSON.parse(savedCart) : [];
|
return savedCart ? JSON.parse(savedCart) : [];
|
||||||
});
|
});
|
||||||
|
const [stockError, setStockError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('cart', JSON.stringify(cart));
|
localStorage.setItem('cart', JSON.stringify(cart));
|
||||||
}, [cart]);
|
}, [cart]);
|
||||||
|
|
||||||
|
const clearStockError = () => setStockError(null);
|
||||||
|
|
||||||
const addToCart = (item: FoodItem) => {
|
const addToCart = (item: FoodItem) => {
|
||||||
setCart((prevCart) => {
|
setCart((prevCart) => {
|
||||||
const existingIndex = prevCart.findIndex((i) => i.id === item.id);
|
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) {
|
if (existingIndex !== -1) {
|
||||||
const newCart = [...prevCart];
|
const newCart = [...prevCart];
|
||||||
newCart[existingIndex] = {
|
newCart[existingIndex] = {
|
||||||
@@ -47,9 +60,17 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
setCart((prevCart) => {
|
setCart((prevCart) => {
|
||||||
const index = prevCart.findIndex((item) => item.id === itemId);
|
const index = prevCart.findIndex((item) => item.id === itemId);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const newCart = [...prevCart];
|
const item = prevCart[index];
|
||||||
const newQty = newCart[index].quantity + delta;
|
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) {
|
if (newQty > 0) {
|
||||||
|
const newCart = [...prevCart];
|
||||||
newCart[index] = { ...newCart[index], quantity: newQty };
|
newCart[index] = { ...newCart[index], quantity: newQty };
|
||||||
return newCart;
|
return newCart;
|
||||||
} else {
|
} else {
|
||||||
@@ -84,6 +105,8 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
getItemQuantity,
|
getItemQuantity,
|
||||||
totalItems,
|
totalItems,
|
||||||
totalPrice,
|
totalPrice,
|
||||||
|
stockError,
|
||||||
|
clearStockError,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async (silent = false) => {
|
||||||
setIsLoading(true);
|
if (!silent) setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const [baseItemsRes, productsRes, stallsRes] = await Promise.all([
|
const [baseItemsRes, productsRes, stallsRes] = await Promise.all([
|
||||||
@@ -119,15 +119,22 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
setFoodItems(mappedFoodItems);
|
setFoodItems(mappedFoodItems);
|
||||||
setStalls(mappedStalls);
|
setStalls(mappedStalls);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message);
|
if (!silent) setError(err.message);
|
||||||
console.error('Error fetching food data:', err);
|
console.error('Error fetching food data:', err);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
if (!silent) setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
|
|
||||||
|
// Silent background polling every 10 seconds to sync stock
|
||||||
|
const pollInterval = setInterval(() => {
|
||||||
|
fetchData(true);
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
return () => clearInterval(pollInterval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Smartphone } from 'lucide-react';
|
import { Smartphone } from 'lucide-react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import Header from '../components/Header';
|
import Header from '../components/Header';
|
||||||
|
import StockConflictModal from '../components/StockConflictModal';
|
||||||
import { useCart } from '../contexts/CartContext';
|
import { useCart } from '../contexts/CartContext';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useFood } from '../contexts/FoodContext';
|
import { useFood } from '../contexts/FoodContext';
|
||||||
@@ -17,11 +19,15 @@ const UPI_APPS = [
|
|||||||
|
|
||||||
const CheckoutScreen: React.FC = () => {
|
const CheckoutScreen: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { cart, totalPrice, clearCart } = useCart();
|
const { cart, totalPrice, clearCart, removeFromCart, updateQuantity } = useCart();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { refreshData } = useFood();
|
const { refreshData } = useFood();
|
||||||
const [selectedApp, setSelectedApp] = useState('gpay');
|
const [selectedApp, setSelectedApp] = useState('gpay');
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
|
||||||
|
// Conflict state
|
||||||
|
const [stockConflicts, setStockConflicts] = useState<any[]>([]);
|
||||||
|
const [showConflictModal, setShowConflictModal] = useState(false);
|
||||||
|
|
||||||
const handlePlaceOrder = async () => {
|
const handlePlaceOrder = async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@@ -41,10 +47,6 @@ const CheckoutScreen: React.FC = () => {
|
|||||||
stallName: item.stallName || null
|
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 {
|
try {
|
||||||
const response = await fetch(`http://${window.location.hostname}:8080/api/orders`, {
|
const response = await fetch(`http://${window.location.hostname}:8080/api/orders`, {
|
||||||
@@ -53,26 +55,23 @@ const CheckoutScreen: React.FC = () => {
|
|||||||
body: JSON.stringify(orderData),
|
body: JSON.stringify(orderData),
|
||||||
});
|
});
|
||||||
|
|
||||||
const contentType = response.headers.get("content-type");
|
const data = await response.json();
|
||||||
if (contentType && contentType.indexOf("application/json") !== -1) {
|
if (data.success) {
|
||||||
const data = await response.json();
|
await refreshData();
|
||||||
if (data.success) {
|
clearCart();
|
||||||
await refreshData();
|
navigate('/success', {
|
||||||
clearCart();
|
state: {
|
||||||
navigate('/success', {
|
orderNumber: data.orderNumber,
|
||||||
state: {
|
displayOrderId: data.displayOrderId
|
||||||
orderNumber: data.orderNumber,
|
}
|
||||||
displayOrderId: data.displayOrderId
|
});
|
||||||
}
|
} else if (data.errorType === 'STOCK_ERROR') {
|
||||||
});
|
console.error('Final Step Stock Conflict:', data.conflicts);
|
||||||
} else {
|
setStockConflicts(data.conflicts || []);
|
||||||
console.error('Order Logic Error:', data.message || data);
|
setShowConflictModal(true);
|
||||||
alert(data.message || 'Failed to place order');
|
await refreshData(true); // Sync background stock
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const text = await response.text();
|
alert(data.message || 'Failed to place order');
|
||||||
console.error('Order Server Error (Non-JSON):', text);
|
|
||||||
alert('Server Error. Check console for details.');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Order error:', 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 (
|
return (
|
||||||
<div className="container checkout-page">
|
<div className="container checkout-page">
|
||||||
<Header title="Checkout" showCart={false} />
|
<Header title="Checkout" showCart={false} />
|
||||||
@@ -141,6 +158,17 @@ const CheckoutScreen: React.FC = () => {
|
|||||||
{isProcessing ? 'Processing...' : `Pay ₹${totalPrice.toFixed(2)} & Place Order`}
|
{isProcessing ? 'Processing...' : `Pay ₹${totalPrice.toFixed(2)} & Place Order`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{showConflictModal && (
|
||||||
|
<StockConflictModal
|
||||||
|
conflicts={stockConflicts}
|
||||||
|
onRemoveItem={handleRemoveConflictItem}
|
||||||
|
onAdjustQuantity={handleAdjustConflictQuantity}
|
||||||
|
onClose={() => navigate('/cart')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -259,3 +259,22 @@
|
|||||||
font-size: 0.9rem;
|
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 React from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { AlertCircle } from 'lucide-react';
|
||||||
import Header from '../components/Header';
|
import Header from '../components/Header';
|
||||||
import { useCart } from '../contexts/CartContext';
|
import { useCart } from '../contexts/CartContext';
|
||||||
import { useFood } from '../contexts/FoodContext';
|
import { useFood } from '../contexts/FoodContext';
|
||||||
@@ -13,6 +14,7 @@ const ItemDetailScreen: React.FC = () => {
|
|||||||
|
|
||||||
const item = foodItems.find((i) => i.id === itemId);
|
const item = foodItems.find((i) => i.id === itemId);
|
||||||
const quantity = item ? getItemQuantity(item.id) : 0;
|
const quantity = item ? getItemQuantity(item.id) : 0;
|
||||||
|
const isLimitReached = item && item.stock !== undefined && quantity >= item.stock && item.stock > 0;
|
||||||
|
|
||||||
if (isLoading && foodItems.length === 0) {
|
if (isLoading && foodItems.length === 0) {
|
||||||
return <div className="container" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Loading...</div>;
|
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">
|
<div className="info-row">
|
||||||
<span className="info-label">Availability</span>
|
<span className="info-label">Availability</span>
|
||||||
<span className={`info-value ${item.stock && item.stock > 0 ? 'green' : 'red'}`}>
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -98,7 +107,7 @@ const ItemDetailScreen: React.FC = () => {
|
|||||||
<span className="quantity">{quantity}</span>
|
<span className="quantity">{quantity}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => addToCart(item)}
|
onClick={() => addToCart(item)}
|
||||||
disabled={item.stock !== undefined && quantity >= item.stock}
|
disabled={isLimitReached}
|
||||||
>+</button>
|
>+</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -65,21 +65,21 @@ const MyOrdersScreen: React.FC = () => {
|
|||||||
const available: FoodItem[] = [];
|
const available: FoodItem[] = [];
|
||||||
|
|
||||||
order.items.forEach(orderItem => {
|
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);
|
const currentItem = foodItems.find(fi => fi.name === orderItem.productName);
|
||||||
|
|
||||||
if (currentItem) {
|
if (currentItem && currentItem.stock !== undefined && currentItem.stock > 0) {
|
||||||
if (currentItem.stock && currentItem.stock > 0) {
|
// Only add up to available stock
|
||||||
// Add multiple times based on original quantity
|
const canAdd = Math.min(orderItem.quantity, currentItem.stock);
|
||||||
for (let i = 0; i < orderItem.quantity; i++) {
|
|
||||||
available.push(currentItem);
|
for (let i = 0; i < canAdd; i++) {
|
||||||
}
|
available.push(currentItem);
|
||||||
} else {
|
}
|
||||||
missing.push(orderItem.productName);
|
|
||||||
|
if (canAdd < orderItem.quantity) {
|
||||||
|
missing.push(`${orderItem.productName} (only ${canAdd} of ${orderItem.quantity} available)`);
|
||||||
}
|
}
|
||||||
} else {
|
} 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