diff --git a/api/src/index.ts b/api/src/index.ts index b991a16..738934b 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -10,6 +10,7 @@ import orderRoutes from './routes/order'; import branchRoutes from './routes/branch'; import headquartersRoutes from './routes/headquarters'; import supplierRoutes from './routes/supplier'; +import cartRoutes from './routes/cart'; const app = express(); const port = process.env.PORT || 3000; @@ -74,6 +75,7 @@ app.use('/api/orders', orderRoutes); app.use('/api/branches', branchRoutes); app.use('/api/headquarters', headquartersRoutes); app.use('/api/suppliers', supplierRoutes); +app.use('/api/carts', cartRoutes); app.get('/', (req, res) => { res.send('Hello, world!'); diff --git a/api/src/models/cart.ts b/api/src/models/cart.ts new file mode 100644 index 0000000..91ec8d2 --- /dev/null +++ b/api/src/models/cart.ts @@ -0,0 +1,42 @@ +/** + * @swagger + * components: + * schemas: + * Cart: + * type: object + * required: + * - cartId + * - userId + * - createdAt + * properties: + * cartId: + * type: integer + * description: The unique identifier for the cart + * userId: + * type: string + * description: The ID of the user who owns the cart + * createdAt: + * type: string + * format: date-time + * description: The date and time when the cart was created + * updatedAt: + * type: string + * format: date-time + * description: The date and time when the cart was last updated + * status: + * type: string + * description: The current status of the cart + * enum: [active, checkout, completed, abandoned] + * totalAmount: + * type: number + * format: float + * description: The total amount of all items in the cart + */ +export interface Cart { + cartId: number; + userId: string; + createdAt: string; + updatedAt: string; + status: 'active' | 'checkout' | 'completed' | 'abandoned'; + totalAmount: number; +} \ No newline at end of file diff --git a/api/src/models/cartItem.ts b/api/src/models/cartItem.ts new file mode 100644 index 0000000..f2e8692 --- /dev/null +++ b/api/src/models/cartItem.ts @@ -0,0 +1,46 @@ +/** + * @swagger + * components: + * schemas: + * CartItem: + * type: object + * required: + * - cartItemId + * - cartId + * - productId + * - quantity + * - unitPrice + * properties: + * cartItemId: + * type: integer + * description: The unique identifier for the cart item + * cartId: + * type: integer + * description: The ID of the parent cart + * productId: + * type: integer + * description: The ID of the product in the cart + * quantity: + * type: integer + * description: The quantity of products in the cart + * unitPrice: + * type: number + * format: float + * description: The price per unit when added to cart + * addedAt: + * type: string + * format: date-time + * description: The date and time when the item was added to the cart + * notes: + * type: string + * description: Additional notes for the cart item + */ +export interface CartItem { + cartItemId: number; + cartId: number; + productId: number; + quantity: number; + unitPrice: number; + addedAt: string; + notes?: string; +} \ No newline at end of file diff --git a/api/src/routes/cart.ts b/api/src/routes/cart.ts new file mode 100644 index 0000000..987d20a --- /dev/null +++ b/api/src/routes/cart.ts @@ -0,0 +1,56 @@ +/** + * @swagger + * tags: + * name: Carts + * description: API endpoints for managing shopping carts + */ + +import express from 'express'; +import { Cart } from '../models/cart'; +import { CartItem } from '../models/cartItem'; +import { Product } from '../models/product'; +import { Order } from '../models/order'; +import { OrderDetail } from '../models/orderDetail'; +import { carts as seedCarts, cartItems as seedCartItems, products as seedProducts, orders as seedOrders, orderDetails as seedOrderDetails } from '../seedData'; + +const router = express.Router(); + +let carts: Cart[] = [...seedCarts]; +let cartItems: CartItem[] = [...seedCartItems]; +let products: Product[] = [...seedProducts]; +let orders: Order[] = [...seedOrders]; +let orderDetails: OrderDetail[] = [...seedOrderDetails]; + +// Create a new cart +router.post('/', (req, res) => { + const { userId } = req.body; + const newCart: Cart = { + cartId: Math.max(0, ...carts.map(c => c.cartId)) + 1, + userId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + status: 'active', + totalAmount: 0 + }; + carts.push(newCart); + res.status(201).json(newCart); +}); + +// Get all carts +router.get('/', (req, res) => { + res.json(carts); +}); + +// Helper function to update cart total +function updateCartTotal(cartId: number) { + const cart = carts.find(c => c.cartId === cartId); + if (!cart) return; + + const items = cartItems.filter(item => item.cartId === cartId); + const total = items.reduce((sum, item) => sum + (item.quantity * item.unitPrice), 0); + + cart.totalAmount = total; + cart.updatedAt = new Date().toISOString(); +} + +export default router; \ No newline at end of file diff --git a/api/src/seedData.ts b/api/src/seedData.ts index ba0bebe..9d17886 100644 --- a/api/src/seedData.ts +++ b/api/src/seedData.ts @@ -6,6 +6,8 @@ import { Order } from './models/order'; import { OrderDetail } from './models/orderDetail'; import { Delivery } from './models/delivery'; import { OrderDetailDelivery } from './models/orderDetailDelivery'; +import { Cart } from './models/cart'; +import { CartItem } from './models/cartItem'; // Suppliers export const suppliers: Supplier[] = [ @@ -291,4 +293,21 @@ export const orderDetailDeliveries: OrderDetailDelivery[] = [ quantity: 20, notes: "Delivery" } +]; + +// Carts +export const carts: Cart[] = [ + { + cartId: 1, + userId: "user1", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + status: 'active', + totalAmount: 0 // Will be calculated dynamically + } +]; + +// Cart Items +export const cartItems: CartItem[] = [ + // Empty initially - items will be added through the API ]; \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d0b02da..88e244e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,8 +5,10 @@ import About from './components/About'; import Footer from './components/Footer'; import Products from './components/entity/product/Products'; import Login from './components/Login'; +import CartPage from './components/cart/CartPage'; import { AuthProvider } from './context/AuthContext'; import { ThemeProvider } from './context/ThemeContext'; +import { CartProvider } from './context/CartContext'; import AdminProducts from './components/admin/AdminProducts'; import { useTheme } from './context/ThemeContext'; @@ -23,6 +25,7 @@ function ThemedApp() { } /> } /> } /> + } /> } /> } /> @@ -37,7 +40,9 @@ function App() { return ( - + + + ); diff --git a/frontend/src/api/cart.ts b/frontend/src/api/cart.ts new file mode 100644 index 0000000..1dbdbd6 --- /dev/null +++ b/frontend/src/api/cart.ts @@ -0,0 +1,88 @@ +import axios from 'axios'; +import { api } from './config'; + +export interface Cart { + cartId: number; + userId: string; + createdAt: string; + updatedAt: string; + status: 'active' | 'checkout' | 'completed' | 'abandoned'; + totalAmount: number; +} + +export interface CartItem { + cartItemId: number; + cartId: number; + productId: number; + quantity: number; + unitPrice: number; + addedAt: string; + notes?: string; +} + +export interface CartWithItems { + cart: Cart; + items: CartItem[]; +} + +export interface Product { + productId: number; + name: string; + description: string; + price: number; + imgName: string; + sku: string; + unit: string; + supplierId: number; + discount?: number; +} + +// Create a new cart +export const createCart = async (userId: string): Promise => { + const { data } = await axios.post(`${api.baseURL}${api.endpoints.carts}`, { userId }); + return data; +}; + +// Get cart by ID +export const getCart = async (cartId: number): Promise => { + const { data } = await axios.get(`${api.baseURL}${api.endpoints.carts}/${cartId}`); + return data; +}; + +// Add item to cart +export const addItemToCart = async (cartId: number, productId: number, quantity: number, notes?: string): Promise => { + const { data } = await axios.post(`${api.baseURL}${api.endpoints.carts}/${cartId}/items`, { + productId, + quantity, + notes + }); + return data; +}; + +// Update cart item quantity +export const updateCartItem = async (cartId: number, itemId: number, quantity: number): Promise => { + const { data } = await axios.put(`${api.baseURL}${api.endpoints.carts}/${cartId}/items/${itemId}`, { + quantity + }); + return data; +}; + +// Remove item from cart +export const removeCartItem = async (cartId: number, itemId: number): Promise => { + await axios.delete(`${api.baseURL}${api.endpoints.carts}/${cartId}/items/${itemId}`); +}; + +// Checkout cart +export const checkoutCart = async (cartId: number, branchId: number, notes?: string): Promise => { + const { data } = await axios.post(`${api.baseURL}${api.endpoints.carts}/${cartId}/checkout`, { + branchId, + notes + }); + return data; +}; + +// Get all products +export const fetchProducts = async (): Promise => { + const { data } = await axios.get(`${api.baseURL}${api.endpoints.products}`); + return data; +}; \ No newline at end of file diff --git a/frontend/src/api/config.ts b/frontend/src/api/config.ts index 6e77299..16239be 100644 --- a/frontend/src/api/config.ts +++ b/frontend/src/api/config.ts @@ -43,6 +43,7 @@ export const api = { headquarters: '/api/headquarters', deliveries: '/api/deliveries', orderDetails: '/api/order-details', - orderDetailDeliveries: '/api/order-detail-deliveries' + orderDetailDeliveries: '/api/order-detail-deliveries', + carts: '/api/carts' } }; \ No newline at end of file diff --git a/frontend/src/components/Navigation.tsx b/frontend/src/components/Navigation.tsx index d7b393b..d44d2c8 100644 --- a/frontend/src/components/Navigation.tsx +++ b/frontend/src/components/Navigation.tsx @@ -2,6 +2,7 @@ import { Link } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import { useTheme } from '../context/ThemeContext'; import { useState } from 'react'; +import CartIcon from './cart/CartIcon'; export default function Navigation() { const { isLoggedIn, isAdmin, logout } = useAuth(); @@ -68,6 +69,9 @@ export default function Navigation() {
+ {/* Cart Icon */} + + +
+ + )} + + {cartItems.length === 0 ? ( +
+ + + +

+ Your cart is empty +

+

+ Add some products to get started! +

+ + Browse Products + +
+ ) : ( +
+ {/* Cart Items */} +
+ {cartItems.map((item) => { + const product = getProductDetails(item.productId); + if (!product) return null; + + return ( +
+ {/* Product Image */} +
+ {product.name} { + const target = e.target as HTMLImageElement; + target.src = '/placeholder-product.png'; + }} + /> +
+ + {/* Product Details */} +
+

+ {product.name} +

+

+ SKU: {product.sku} +

+

+ ${item.unitPrice.toFixed(2)} each +

+
+ + {/* Quantity Controls */} +
+ + + {item.quantity} + + +
+ + {/* Item Total */} +
+

+ ${(item.quantity * item.unitPrice).toFixed(2)} +

+
+ + {/* Remove Button */} + +
+ ); + })} +
+ + {/* Cart Summary */} +
+
+ + Total ({cartCount} items): + + + ${cartTotal.toFixed(2)} + +
+ +
+ + Continue Shopping + + +
+
+
+ )} + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/entity/product/Products.tsx b/frontend/src/components/entity/product/Products.tsx index af2319e..1380363 100644 --- a/frontend/src/components/entity/product/Products.tsx +++ b/frontend/src/components/entity/product/Products.tsx @@ -3,6 +3,7 @@ import axios from 'axios'; import { useQuery } from 'react-query'; import { api } from '../../../api/config'; import { useTheme } from '../../../context/ThemeContext'; +import { useCart } from '../../../context/CartContext'; interface Product { productId: number; @@ -28,6 +29,7 @@ export default function Products() { const [showModal, setShowModal] = useState(false); const { data: products, isLoading, error } = useQuery('products', fetchProducts); const { darkMode } = useTheme(); + const { addToCart, isLoading: cartLoading } = useCart(); const filteredProducts = products?.filter(product => product.name.toLowerCase().includes(searchTerm.toLowerCase()) || @@ -41,15 +43,21 @@ export default function Products() { })); }; - const handleAddToCart = (productId: number) => { + const handleAddToCart = async (productId: number) => { const quantity = quantities[productId] || 0; if (quantity > 0) { - // TODO: Implement cart functionality - alert(`Added ${quantity} items to cart`); - setQuantities(prev => ({ - ...prev, - [productId]: 0 - })); + try { + await addToCart(productId, quantity); + // Reset quantity to 0 after successful add + setQuantities(prev => ({ + ...prev, + [productId]: 0 + })); + // You could add a success message here + } catch (err) { + console.error('Failed to add to cart:', err); + // Error handling is done in the cart context + } } }; @@ -171,15 +179,15 @@ export default function Products() { diff --git a/frontend/src/context/CartContext.tsx b/frontend/src/context/CartContext.tsx new file mode 100644 index 0000000..4b89896 --- /dev/null +++ b/frontend/src/context/CartContext.tsx @@ -0,0 +1,169 @@ +import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { Cart, CartItem, createCart, getCart, addItemToCart, updateCartItem, removeCartItem } from '../api/cart'; + +interface CartContextType { + cart: Cart | null; + cartItems: CartItem[]; + cartCount: number; + cartTotal: number; + isLoading: boolean; + error: string | null; + addToCart: (productId: number, quantity: number) => Promise; + updateItemQuantity: (itemId: number, quantity: number) => Promise; + removeFromCart: (itemId: number) => Promise; + clearError: () => void; + refreshCart: () => Promise; +} + +const CartContext = createContext(null); + +export function useCart() { + const context = useContext(CartContext); + if (!context) { + throw new Error('useCart must be used within a CartProvider'); + } + return context; +} + +export function CartProvider({ children }: { children: ReactNode }) { + const [cart, setCart] = useState(null); + const [cartItems, setCartItems] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Initialize cart + useEffect(() => { + initializeCart(); + }, []); + + const initializeCart = async () => { + try { + setIsLoading(true); + + // Check if we have a cart ID in localStorage + const savedCartId = localStorage.getItem('cartId'); + + if (savedCartId) { + // Try to load existing cart + try { + const cartData = await getCart(parseInt(savedCartId)); + setCart(cartData.cart); + setCartItems(cartData.items); + } catch (err) { + // If cart doesn't exist, create a new one + console.log('Saved cart not found, creating new cart'); + await createNewCart(); + } + } else { + // Create new cart + await createNewCart(); + } + } catch (err) { + console.error('Error initializing cart:', err); + setError('Failed to initialize cart'); + } finally { + setIsLoading(false); + } + }; + + const createNewCart = async () => { + const userId = 'user1'; // For demo purposes, using a fixed user ID + const newCart = await createCart(userId); + setCart(newCart); + setCartItems([]); + localStorage.setItem('cartId', newCart.cartId.toString()); + }; + + const refreshCart = async () => { + if (!cart) return; + + try { + const cartData = await getCart(cart.cartId); + setCart(cartData.cart); + setCartItems(cartData.items); + } catch (err) { + console.error('Error refreshing cart:', err); + setError('Failed to refresh cart'); + } + }; + + const addToCart = async (productId: number, quantity: number) => { + if (!cart) { + setError('Cart not initialized'); + return; + } + + try { + setIsLoading(true); + await addItemToCart(cart.cartId, productId, quantity); + await refreshCart(); + } catch (err) { + console.error('Error adding to cart:', err); + setError('Failed to add item to cart'); + } finally { + setIsLoading(false); + } + }; + + const updateItemQuantity = async (itemId: number, quantity: number) => { + if (!cart) return; + + try { + setIsLoading(true); + if (quantity <= 0) { + await removeCartItem(cart.cartId, itemId); + } else { + await updateCartItem(cart.cartId, itemId, quantity); + } + await refreshCart(); + } catch (err) { + console.error('Error updating cart item:', err); + setError('Failed to update cart item'); + } finally { + setIsLoading(false); + } + }; + + const removeFromCart = async (itemId: number) => { + if (!cart) return; + + try { + setIsLoading(true); + await removeCartItem(cart.cartId, itemId); + await refreshCart(); + } catch (err) { + console.error('Error removing from cart:', err); + setError('Failed to remove item from cart'); + } finally { + setIsLoading(false); + } + }; + + const clearError = () => { + setError(null); + }; + + // Calculate cart count and total + const cartCount = cartItems.reduce((sum, item) => sum + item.quantity, 0); + const cartTotal = cartItems.reduce((sum, item) => sum + (item.quantity * item.unitPrice), 0); + + const value: CartContextType = { + cart, + cartItems, + cartCount, + cartTotal, + isLoading, + error, + addToCart, + updateItemQuantity, + removeFromCart, + clearError, + refreshCart + }; + + return ( + + {children} + + ); +} \ No newline at end of file