Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions api/src/routes/delivery.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
23 changes: 8 additions & 15 deletions api/src/routes/delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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');
}
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -24,6 +26,7 @@ function ThemedApp() {
<Route path="/about" element={<About />} />
<Route path="/products" element={<Products />} />
<Route path="/login" element={<Login />} />
<Route path="/cart" element={<Cart />} />
<Route path="/admin/products" element={<AdminProducts />} />
</Routes>
</main>
Expand All @@ -37,7 +40,9 @@ function App() {
return (
<AuthProvider>
<ThemeProvider>
<ThemedApp />
<CartProvider>
<ThemedApp />
</CartProvider>
</ThemeProvider>
</AuthProvider>
);
Expand Down
114 changes: 114 additions & 0 deletions frontend/src/components/Cart.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`min-h-screen ${darkMode ? 'bg-dark' : 'bg-gray-100'} pt-20 px-4 transition-colors duration-300`}>
<div className="max-w-3xl mx-auto py-12 text-center">
<svg className="mx-auto h-16 w-16 text-gray-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<h2 className={`text-2xl font-bold ${darkMode ? 'text-light' : 'text-gray-800'} mb-2`}>Your cart is empty</h2>
<p className={`${darkMode ? 'text-gray-400' : 'text-gray-500'}`}>Add items from the Products page to get started.</p>
</div>
</div>
);
}

return (
<div className={`min-h-screen ${darkMode ? 'bg-dark' : 'bg-gray-100'} pt-20 pb-16 px-4 transition-colors duration-300`}>
<div className="max-w-3xl mx-auto">
<div className="flex justify-between items-center mb-6">
<h1 className={`text-3xl font-bold ${darkMode ? 'text-light' : 'text-gray-800'}`}>
Cart ({totalItems} {totalItems === 1 ? 'item' : 'items'})
</h1>
<button
onClick={clearCart}
className={`text-sm ${darkMode ? 'text-gray-400 hover:text-red-400' : 'text-gray-500 hover:text-red-600'} transition-colors`}
>
Clear cart
</button>
</div>

<div className={`${darkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg shadow-lg overflow-hidden divide-y ${darkMode ? 'divide-gray-700' : 'divide-gray-200'}`}>
{items.map(item => {
const effectivePrice = item.discount ? item.price * (1 - item.discount) : item.price;
return (
<div key={item.productId} className="flex items-center p-4 gap-4">
<img
src={`/${item.imgName}`}
alt={item.name}
className={`h-20 w-20 object-contain rounded ${darkMode ? 'bg-gray-700' : 'bg-gray-100'} p-1 flex-shrink-0`}
/>
<div className="flex-grow min-w-0">
<h3 className={`font-semibold ${darkMode ? 'text-light' : 'text-gray-800'} truncate`}>{item.name}</h3>
<div className="flex items-center gap-2 mt-1">
{item.discount && (
<span className="text-gray-500 line-through text-sm">${item.price.toFixed(2)}</span>
)}
<span className="text-primary font-bold">${effectivePrice.toFixed(2)}</span>
</div>
<div className={`flex items-center space-x-2 mt-2 ${darkMode ? 'bg-gray-700' : 'bg-gray-200'} rounded-lg p-1 w-fit`}>
<button
onClick={() => updateQuantity(item.productId, item.quantity - 1)}
className={`w-7 h-7 flex items-center justify-center ${darkMode ? 'text-light' : 'text-gray-700'} hover:text-primary transition-colors`}
aria-label={`Decrease quantity of ${item.name}`}
>
<span aria-hidden="true">-</span>
</button>
<span className={`${darkMode ? 'text-light' : 'text-gray-800'} min-w-[2rem] text-center`}>{item.quantity}</span>
<button
onClick={() => updateQuantity(item.productId, item.quantity + 1)}
className={`w-7 h-7 flex items-center justify-center ${darkMode ? 'text-light' : 'text-gray-700'} hover:text-primary transition-colors`}
aria-label={`Increase quantity of ${item.name}`}
>
<span aria-hidden="true">+</span>
</button>
</div>
</div>
<div className="flex flex-col items-end gap-2 flex-shrink-0">
<span className={`font-bold ${darkMode ? 'text-light' : 'text-gray-800'}`}>
${(effectivePrice * item.quantity).toFixed(2)}
</span>
<button
onClick={() => removeItem(item.productId)}
className="text-red-500 hover:text-red-700 transition-colors"
aria-label={`Remove ${item.name} from cart`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
);
})}
</div>

<div className={`mt-6 ${darkMode ? 'bg-gray-800' : 'bg-white'} rounded-lg shadow-lg p-6`}>
<div className="flex justify-between items-center mb-4">
<span className={`text-lg font-semibold ${darkMode ? 'text-light' : 'text-gray-800'}`}>Total</span>
<span className="text-2xl font-bold text-primary">${totalPrice.toFixed(2)}</span>
</div>
{checkoutMessage && (
<div className="mb-4 p-3 rounded-lg bg-primary/10 text-primary text-sm text-center">
Checkout functionality coming soon!
</div>
)}
<button
className="w-full py-3 bg-primary hover:bg-accent text-white font-semibold rounded-lg transition-colors"
onClick={() => setCheckoutMessage(true)}
>
Proceed to Checkout
</button>
</div>
</div>
</div>
);
}
19 changes: 19 additions & 0 deletions frontend/src/components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -68,6 +70,23 @@ export default function Navigation() {
</div>
</div>
<div className="flex items-center space-x-4">
<Link
to="/cart"
className="relative p-2 rounded-full focus:outline-none transition-colors"
aria-label="View cart"
>
<svg xmlns="http://www.w3.org/2000/svg" className={`h-5 w-5 ${darkMode ? 'text-light' : 'text-gray-700'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
{totalItems > 0 && (
<span
className="absolute -top-1 -right-1 bg-primary text-white text-xs font-bold rounded-full h-5 w-5 flex items-center justify-center"
aria-label={`${totalItems} items in cart`}
>
{totalItems > 99 ? '99+' : totalItems}
</span>
)}
</Link>
<button
onClick={toggleTheme}
className="p-2 rounded-full focus:outline-none transition-colors"
Expand Down
19 changes: 13 additions & 6 deletions frontend/src/components/entity/product/Products.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 { addItem } = useCart();

const filteredProducts = products?.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
Expand All @@ -41,14 +43,19 @@ export default function Products() {
}));
};

const handleAddToCart = (productId: number) => {
const quantity = quantities[productId] || 0;
const handleAddToCart = (product: Product) => {
const quantity = quantities[product.productId] || 0;
if (quantity > 0) {
// TODO: Implement cart functionality
alert(`Added ${quantity} items to cart`);
addItem({
productId: product.productId,
name: product.name,
price: product.price,
imgName: product.imgName,
discount: product.discount,
}, quantity);
setQuantities(prev => ({
...prev,
[productId]: 0
[product.productId]: 0
}));
}
};
Expand Down Expand Up @@ -169,7 +176,7 @@ export default function Products() {
</button>
</div>
<button
onClick={() => handleAddToCart(product.productId)}
onClick={() => handleAddToCart(product)}
className={`px-4 py-2 rounded-lg transition-colors ${
quantities[product.productId]
? 'bg-primary hover:bg-accent text-white'
Expand Down
Loading