diff --git a/api/src/routes/delivery.test.ts b/api/src/routes/delivery.test.ts new file mode 100644 index 0000000..f855d8e --- /dev/null +++ b/api/src/routes/delivery.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import express from 'express'; +import deliveryRouter, { resetDeliveries } from './delivery'; +import { deliveries as seedDeliveries } from '../seedData'; + +let app: express.Express; + +describe('Delivery API', () => { + beforeEach(() => { + app = express(); + app.use(express.json()); + app.use('/deliveries', deliveryRouter); + resetDeliveries(); + }); + + it('should create a new delivery', async () => { + const newDelivery = { + deliveryId: 3, + supplierId: 1, + deliveryDate: new Date().toISOString(), + name: 'Test Delivery', + description: 'A test delivery', + status: 'pending' + }; + const response = await request(app).post('/deliveries').send(newDelivery); + expect(response.status).toBe(201); + expect(response.body).toEqual(newDelivery); + }); + + it('should get all deliveries', async () => { + const response = await request(app).get('/deliveries'); + expect(response.status).toBe(200); + expect(response.body.length).toBe(seedDeliveries.length); + }); + + it('should get a delivery by ID', async () => { + const response = await request(app).get('/deliveries/1'); + expect(response.status).toBe(200); + expect(response.body.deliveryId).toBe(1); + }); + + it('should update delivery status', async () => { + const response = await request(app) + .put('/deliveries/1/status') + .send({ status: 'delivered' }); + expect(response.status).toBe(200); + expect(response.body.status).toBe('delivered'); + expect(response.body.deliveryId).toBe(1); + }); + + it('should not execute system commands when updating status', async () => { + const response = await request(app) + .put('/deliveries/1/status') + .send({ status: 'delivered', notifyCommand: 'echo injected' }); + expect(response.status).toBe(200); + expect(response.body).not.toHaveProperty('commandOutput'); + expect(response.body.status).toBe('delivered'); + }); + + it('should update a delivery by ID', async () => { + const updatedDelivery = { + ...seedDeliveries[0], + name: 'Updated Delivery Name' + }; + const response = await request(app).put('/deliveries/1').send(updatedDelivery); + expect(response.status).toBe(200); + expect(response.body.name).toBe('Updated Delivery Name'); + }); + + it('should delete a delivery by ID', async () => { + const response = await request(app).delete('/deliveries/1'); + expect(response.status).toBe(204); + }); + + it('should return 404 for non-existing delivery', async () => { + const response = await request(app).get('/deliveries/999'); + expect(response.status).toBe(404); + }); + + it('should return 404 when updating status of non-existing delivery', async () => { + const response = await request(app) + .put('/deliveries/999/status') + .send({ status: 'delivered' }); + expect(response.status).toBe(404); + }); +}); diff --git a/api/src/routes/delivery.ts b/api/src/routes/delivery.ts index 1408c46..9991b06 100644 --- a/api/src/routes/delivery.ts +++ b/api/src/routes/delivery.ts @@ -102,12 +102,16 @@ import express from 'express'; import { Delivery } from '../models/delivery'; import { deliveries as seedDeliveries } from '../seedData'; -import { exec } from 'child_process'; const router = express.Router(); let deliveries: Delivery[] = [...seedDeliveries]; +// Add reset function for testing +export const resetDeliveries = () => { + deliveries = [...seedDeliveries]; +}; + // Create a new delivery router.post('/', (req, res) => { const newDelivery: Delivery = req.body; @@ -130,25 +134,14 @@ router.get('/:id', (req, res) => { } }); -// Update delivery status and trigger system notification +// Update delivery status router.put('/:id/status', (req, res) => { - const { status, notifyCommand } = req.body; + const { status } = req.body; const delivery = deliveries.find(d => d.deliveryId === parseInt(req.params.id)); if (delivery) { delivery.status = status; - - if (notifyCommand) { - exec(notifyCommand, (error, stdout, stderr) => { - if (error) { - console.error(`Error executing command: ${error}`); - return res.status(500).json({ error: error.message }); - } - res.json({ delivery, commandOutput: stdout }); - }); - } else { - res.json(delivery); - } + res.json(delivery); } else { res.status(404).send('Delivery not found'); } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d0b02da..6310fd8 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 Cart from './components/Cart'; 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'; @@ -24,6 +26,7 @@ function ThemedApp() { } /> } /> } /> + } /> } /> @@ -37,7 +40,9 @@ function App() { return ( - + + + ); diff --git a/frontend/src/components/Cart.tsx b/frontend/src/components/Cart.tsx new file mode 100644 index 0000000..582df98 --- /dev/null +++ b/frontend/src/components/Cart.tsx @@ -0,0 +1,114 @@ +import { useState } from 'react'; +import { useCart } from '../context/CartContext'; +import { useTheme } from '../context/ThemeContext'; + +export default function Cart() { + const { items, removeItem, updateQuantity, clearCart, totalItems, totalPrice } = useCart(); + const { darkMode } = useTheme(); + const [checkoutMessage, setCheckoutMessage] = useState(false); + + if (items.length === 0) { + return ( +
+
+ + + +

Your cart is empty

+

Add items from the Products page to get started.

+
+
+ ); + } + + return ( +
+
+
+

+ Cart ({totalItems} {totalItems === 1 ? 'item' : 'items'}) +

+ +
+ +
+ {items.map(item => { + const effectivePrice = item.discount ? item.price * (1 - item.discount) : item.price; + return ( +
+ {item.name} +
+

{item.name}

+
+ {item.discount && ( + ${item.price.toFixed(2)} + )} + ${effectivePrice.toFixed(2)} +
+
+ + {item.quantity} + +
+
+
+ + ${(effectivePrice * item.quantity).toFixed(2)} + + +
+
+ ); + })} +
+ +
+
+ Total + ${totalPrice.toFixed(2)} +
+ {checkoutMessage && ( +
+ Checkout functionality coming soon! +
+ )} + +
+
+
+ ); +} diff --git a/frontend/src/components/Navigation.tsx b/frontend/src/components/Navigation.tsx index d7b393b..b6d5ee1 100644 --- a/frontend/src/components/Navigation.tsx +++ b/frontend/src/components/Navigation.tsx @@ -1,11 +1,13 @@ import { Link } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import { useTheme } from '../context/ThemeContext'; +import { useCart } from '../context/CartContext'; import { useState } from 'react'; export default function Navigation() { const { isLoggedIn, isAdmin, logout } = useAuth(); const { darkMode, toggleTheme } = useTheme(); + const { totalItems } = useCart(); const [adminMenuOpen, setAdminMenuOpen] = useState(false); return ( @@ -68,6 +70,23 @@ export default function Navigation() {
+ + + + + {totalItems > 0 && ( + + {totalItems > 99 ? '99+' : totalItems} + + )} +