diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java index 44f92d63..2941f99a 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java @@ -155,6 +155,15 @@ public class OrderController { // Use offer price if present, otherwise base price BigDecimal unitPrice = (product.getOfferPrice() != null && product.getOfferPrice().compareTo(BigDecimal.ZERO) > 0) ? product.getOfferPrice() : product.getPrice(); + + // Add parcel fee if selected and parcellable + if (item.getProductName() != null && item.getProductName().endsWith(" (Parcel)") && product.isParcellable()) { + unitPrice = unitPrice.add(BigDecimal.valueOf(5)); + } + + // Update item's saved price so it reflects the unitPrice + parcel fee + item.setPrice(unitPrice); + serverTotal = serverTotal.add(unitPrice.multiply(BigDecimal.valueOf(item.getQuantity()))); } } diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/ProductController.java b/backend/src/main/java/com/rit/canteen/sales/controller/ProductController.java index 26be11cb..d641e0d4 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/ProductController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/ProductController.java @@ -99,6 +99,7 @@ public class ProductController { product.setVeg(productDetails.isVeg()); product.setHasAllergy(productDetails.isHasAllergy()); product.setParcelNotAllowed(productDetails.isParcelNotAllowed()); + product.setParcellable(productDetails.isParcellable()); product.setSessionOptional(productDetails.isSessionOptional()); product.getSessions().clear(); @@ -238,6 +239,7 @@ public class ProductController { live.setImageData(productDetails.getImageData()); live.setDraft(false); live.setActive(true); + live.setParcellable(productDetails.isParcellable()); Product savedLive = productRepository.save(live); productRepository.delete(product); // Delete the draft @@ -257,6 +259,7 @@ public class ProductController { product.setBarcode(productDetails.getBarcode()); product.setImageData(productDetails.getImageData()); product.setStock(productDetails.getStock()); + product.setParcellable(productDetails.isParcellable()); product.getSessions().clear(); if (productDetails.getSessions() != null) { diff --git a/backend/src/main/java/com/rit/canteen/sales/model/Product.java b/backend/src/main/java/com/rit/canteen/sales/model/Product.java index 47e2eef2..b3295b2f 100644 --- a/backend/src/main/java/com/rit/canteen/sales/model/Product.java +++ b/backend/src/main/java/com/rit/canteen/sales/model/Product.java @@ -49,11 +49,13 @@ public class Product { private boolean attributesOptional; // Additional Attributes - private boolean isVeg; + private boolean isVeg = true; private boolean hasAllergy; private boolean parcelNotAllowed; + private boolean parcellable; + private boolean sessionOptional; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @@ -133,6 +135,9 @@ public class Product { public boolean isParcelNotAllowed() { return parcelNotAllowed; } public void setParcelNotAllowed(boolean parcelNotAllowed) { this.parcelNotAllowed = parcelNotAllowed; } + public boolean isParcellable() { return parcellable; } + public void setParcellable(boolean parcellable) { this.parcellable = parcellable; } + public boolean isSessionOptional() { return sessionOptional; } public void setSessionOptional(boolean sessionOptional) { this.sessionOptional = sessionOptional; } diff --git a/frontend/src/pages/NewArrivals.tsx b/frontend/src/pages/NewArrivals.tsx index 3350b83d..f5afb481 100644 --- a/frontend/src/pages/NewArrivals.tsx +++ b/frontend/src/pages/NewArrivals.tsx @@ -38,6 +38,7 @@ interface Product { active: boolean; stock: number; stalls?: { id: number; name: string }[]; + parcellable?: boolean; } interface Stall { diff --git a/frontend/src/pages/Products.tsx b/frontend/src/pages/Products.tsx index f6ba2d35..1732a7e0 100644 --- a/frontend/src/pages/Products.tsx +++ b/frontend/src/pages/Products.tsx @@ -1,4 +1,4 @@ -ο»Ώimport { apiFetch } from '../api'; +import { apiFetch } from '../api'; import React, { useState, useEffect, useRef } from 'react'; import { Plus, X, Search, Filter, MoreVertical, RefreshCw, Edit2, Power, PowerOff, Tag, Package, Image as ImageIcon, Barcode, DollarSign, ChevronDown, Clock, Check, Trash2, Database } from 'lucide-react'; import Pagination from '../components/Pagination'; @@ -36,6 +36,7 @@ interface Product { active: boolean; stock: number; stalls?: { id: number; name: string }[]; + parcellable?: boolean; } interface Stall { @@ -69,14 +70,15 @@ const emptyProduct: Product = { parcelCharges: 0, barcode: '', attributesOptional: false, - veg: false, + veg: true, hasAllergy: false, parcelNotAllowed: false, sessionOptional: false, sessions: getDefaultSessions(), imageData: '', active: true, - stock: 0 + stock: 0, + parcellable: false }; const Products = () => { @@ -529,16 +531,29 @@ const Products = () => {
-
-

Additional Attributes

- +

Additional Attributes

+
+ +
- {formData.attributesOptional && ( -
- - -
- )} +
+ +
+
diff --git a/ordering_site/src/components/ItemCard.tsx b/ordering_site/src/components/ItemCard.tsx index aa8179d6..2b887b83 100644 --- a/ordering_site/src/components/ItemCard.tsx +++ b/ordering_site/src/components/ItemCard.tsx @@ -11,6 +11,14 @@ interface ItemCardProps { variant?: 'carousel' | 'list'; } +export const VegNonVegIcon: React.FC<{ isVeg: boolean }> = ({ isVeg }) => { + return ( +
+
+
+ ); +}; + const ItemCard: React.FC = ({ item, isLast, variant = 'list' }) => { const navigate = useNavigate(); const { addToCart, updateQuantity, getItemQuantity } = useCart(); @@ -18,7 +26,8 @@ const ItemCard: React.FC = ({ item, isLast, variant = 'list' }) = const isLimitReached = item.stock !== undefined && quantity >= item.stock && item.stock > 0; // Visual helper values matching mockup metadata - const rating = 4.5; + const rating = item.rating !== undefined ? item.rating : 5.0; + const ratingCount = item.ratingCount !== undefined ? item.ratingCount : 0; if (variant === 'carousel') { return ( @@ -26,10 +35,12 @@ const ItemCard: React.FC = ({ item, isLast, variant = 'list' }) = className={`item-card-carousel ${item.stock === 0 ? 'out-of-stock' : ''}`} onClick={() => navigate(`/item/${item.id}`)} > -
- - {rating} -
+ {ratingCount > 0 && ( +
+ + {rating.toFixed(1)} +
+ )}
{item.image ? ( @@ -40,8 +51,11 @@ const ItemCard: React.FC = ({ item, isLast, variant = 'list' }) =
-

{item.name}

-

{item.stallName || 'Cookie Heaven'}

+
+ +

{item.name}

+
+

{item.stallName || 'Unknown Stall'}

e.stopPropagation()}> πŸ…‘{item.price.toFixed(2)} @@ -89,15 +103,19 @@ const ItemCard: React.FC = ({ item, isLast, variant = 'list' }) =
-

{item.name}

-
- - {rating} +
+ +

{item.name}

+ {ratingCount > 0 && ( +
+ + {rating.toFixed(1)} ({ratingCount}) +
+ )}
-

{item.stallName || 'Cookie Heaven'}

-

πŸ“ 54 Summit Street

+

{item.stallName || 'Unknown Stall'}

e.stopPropagation()}> πŸ…‘{item.price.toFixed(2)} diff --git a/ordering_site/src/contexts/CartContext.tsx b/ordering_site/src/contexts/CartContext.tsx index 94b7d000..f11f008b 100644 --- a/ordering_site/src/contexts/CartContext.tsx +++ b/ordering_site/src/contexts/CartContext.tsx @@ -3,9 +3,10 @@ import type { FoodItem, CartItem } from '../types'; interface CartContextType { cart: CartItem[]; - addToCart: (item: FoodItem) => void; + addToCart: (item: FoodItem, isParcel?: boolean) => void; removeFromCart: (itemId: string) => void; updateQuantity: (itemId: string, delta: number) => void; + toggleParcel: (itemId: string) => void; clearCart: () => void; getItemQuantity: (itemId: string) => number; totalItems: number; @@ -29,7 +30,7 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children const clearStockError = () => setStockError(null); - const addToCart = (item: FoodItem) => { + const addToCart = (item: FoodItem, isParcel?: boolean) => { setCart((prevCart) => { const existingIndex = prevCart.findIndex((i) => i.id === item.id); @@ -45,13 +46,22 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children newCart[existingIndex] = { ...newCart[existingIndex], quantity: newCart[existingIndex].quantity + 1, + isParcel: isParcel !== undefined ? isParcel : newCart[existingIndex].isParcel }; return newCart; } - return [...prevCart, { ...item, quantity: 1 }]; + return [...prevCart, { ...item, quantity: 1, isParcel: !!isParcel }]; }); }; + const toggleParcel = (itemId: string) => { + setCart((prevCart) => + prevCart.map((item) => + item.id === itemId ? { ...item, isParcel: !item.isParcel } : item + ) + ); + }; + const removeFromCart = (itemId: string) => { setCart((prevCart) => prevCart.filter((item) => item.id !== itemId)); }; @@ -92,7 +102,7 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }; const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0); - const totalPrice = cart.reduce((sum, item) => sum + item.price * item.quantity, 0); + const totalPrice = cart.reduce((sum, item) => sum + (item.price + (item.isParcel ? 5 : 0)) * item.quantity, 0); return ( = ({ children addToCart, removeFromCart, updateQuantity, + toggleParcel, clearCart, getItemQuantity, totalItems, diff --git a/ordering_site/src/contexts/FoodContext.tsx b/ordering_site/src/contexts/FoodContext.tsx index d7b581d2..660cc9ba 100644 --- a/ordering_site/src/contexts/FoodContext.tsx +++ b/ordering_site/src/contexts/FoodContext.tsx @@ -28,11 +28,20 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children if (!silent) setIsLoading(true); setError(null); try { + const token = localStorage.getItem('token'); + const headers: Record = {}; + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + const [baseItemsRes, productsRes, stallsRes, statsRes] = await Promise.all([ fetch(`${API_BASE_URL}/base-items?size=100`, { cache: 'no-store' }), fetch(`${API_BASE_URL}/products?size=100`, { cache: 'no-store' }), fetch(`${API_BASE_URL}/stalls/active`, { cache: 'no-store' }), - fetch(`${API_BASE_URL}/feedback/stats`, { cache: 'no-store' }).catch(() => null) + fetch(`${API_BASE_URL}/feedback/stats`, { + cache: 'no-store', + headers + }).catch(() => null) ]); if (!baseItemsRes.ok || !productsRes.ok || !stallsRes.ok) { @@ -46,8 +55,9 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children const baseItemsData = baseItemsDataRaw.content || baseItemsDataRaw; const productsData = productsDataRaw.content || productsDataRaw; - // Extract top rated items names by customer feedback count + // Extract top rated items names by customer feedback count and rating maps let topRatedNames: string[] = []; + const itemRatingsMap: Record = {}; if (statsRes && statsRes.ok) { try { const statsData = await statsRes.json(); @@ -59,6 +69,15 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children .filter((x: any) => x.count > 0) .slice(0, 3) .map((x: any) => x.name.toLowerCase()); + + ratedItems.forEach((x: any) => { + if (x.name) { + itemRatingsMap[x.name.toLowerCase()] = { + average: x.average, + count: x.count + }; + } + }); } catch (e) { console.error('Error parsing feedback stats:', e); } @@ -121,6 +140,7 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children // Dynamic bestseller isPopular flag based on feedback stats const isBestseller = topRatedNames.includes(item.name.toLowerCase()); + const ratingInfo = itemRatingsMap[item.name.toLowerCase()]; return { id: itemId, @@ -133,7 +153,10 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children isPopular: isBestseller, stock: item.stock, stallId: (stallFromBackend?.id || stallFromMap?.id || stallFromCategory?.id), - stallName: (stallFromBackend?.name || stallFromMap?.name || stallFromCategory?.name) + stallName: (stallFromBackend?.name || stallFromMap?.name || stallFromCategory?.name), + rating: ratingInfo ? ratingInfo.average : 5.0, + ratingCount: ratingInfo ? ratingInfo.count : 0, + parcellable: item.parcellable }; }); diff --git a/ordering_site/src/index.css b/ordering_site/src/index.css index 74d159ea..577f11c7 100644 --- a/ordering_site/src/index.css +++ b/ordering_site/src/index.css @@ -232,3 +232,38 @@ main::-webkit-scrollbar { .latest-qr-wrapper.qr-expired canvas { filter: blur(6px) grayscale(100%) !important; } + +/* Veg / Non-Veg Indicator Styling */ +.veg-nonveg-indicator { + width: 14px; + height: 14px; + border: 1.5px solid; + border-radius: 3px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + background-color: transparent; +} + +.veg-nonveg-indicator.veg { + border-color: #10b981; /* emerald-500 */ +} + +.veg-nonveg-indicator.non-veg { + border-color: #ef4444; /* red-500 */ +} + +.veg-nonveg-indicator .dot { + width: 6px; + height: 6px; + border-radius: 50%; +} + +.veg-nonveg-indicator.veg .dot { + background-color: #10b981; +} + +.veg-nonveg-indicator.non-veg .dot { + background-color: #ef4444; +} diff --git a/ordering_site/src/pages/CartScreen.tsx b/ordering_site/src/pages/CartScreen.tsx index be663fea..0e36194d 100644 --- a/ordering_site/src/pages/CartScreen.tsx +++ b/ordering_site/src/pages/CartScreen.tsx @@ -7,7 +7,7 @@ import './CartScreen.css'; const CartScreen: React.FC = () => { const navigate = useNavigate(); - const { cart, updateQuantity, removeFromCart, totalPrice, totalItems } = useCart(); + const { cart, updateQuantity, removeFromCart, toggleParcel, totalPrice, totalItems } = useCart(); if (cart.length === 0) { return ( @@ -45,8 +45,23 @@ const CartScreen: React.FC = () => {
+ {item.parcellable && ( +
+ +
+ )} +
- πŸ…‘{(item.price * item.quantity).toFixed(2)} + πŸ…‘{((item.price + (item.isParcel ? 5 : 0)) * item.quantity).toFixed(2)}
{quantity} @@ -130,22 +163,48 @@ const ItemDetailScreen: React.FC = () => {
-
-
- - {rating} + {ratingCount > 0 ? ( +
+
+ + {rating.toFixed(1)} ({ratingCount} {ratingCount === 1 ? 'review' : 'reviews'}) +
-
+ ) : ( +
+
+ + No reviews yet +
+
+ )}

{item.longDescription || item.description || 'Quality food prepared with fresh ingredients, crafted with care for a premium taste experience.'}

-
+ {item.parcellable && ( +
+ +
+ )} +
Category @@ -175,7 +234,7 @@ const ItemDetailScreen: React.FC = () => {