rename grocery to product

This commit is contained in:
2025-05-26 20:20:21 +02:00
parent 1b984d18d9
commit d27871160e
26 changed files with 1114 additions and 498 deletions

View File

@@ -1,67 +1,64 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import GroceryList from './components/GroceryList';
import ShopList from './components/ShopList';
import ShoppingEventForm from './components/ShoppingEventForm';
import ShoppingEventList from './components/ShoppingEventList';
import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom';
import Dashboard from './components/Dashboard';
import ProductList from './components/ProductList';
import ShopList from './components/ShopList';
import ShoppingEventList from './components/ShoppingEventList';
import ShoppingEventForm from './components/ShoppingEventForm';
function Navigation() {
const location = useLocation();
const isActive = (path: string) => {
return location.pathname === path;
};
return (
<nav className="bg-blue-600 text-white p-4">
<div className="container mx-auto flex justify-between items-center">
<Link to="/" className="text-xl font-bold">
Product Tracker
</Link>
<div className="space-x-4">
<Link
to="/"
className={`px-3 py-2 rounded ${isActive('/') ? 'bg-blue-800' : 'hover:bg-blue-700'}`}
>
Dashboard
</Link>
<Link
to="/products"
className={`px-3 py-2 rounded ${isActive('/products') ? 'bg-blue-800' : 'hover:bg-blue-700'}`}
>
Products
</Link>
<Link
to="/shops"
className={`px-3 py-2 rounded ${isActive('/shops') ? 'bg-blue-800' : 'hover:bg-blue-700'}`}
>
Shops
</Link>
<Link
to="/shopping-events"
className={`px-3 py-2 rounded ${isActive('/shopping-events') ? 'bg-blue-800' : 'hover:bg-blue-700'}`}
>
Shopping Events
</Link>
</div>
</div>
</nav>
);
}
function App() {
return (
<Router>
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<nav className="bg-white shadow-lg">
<div className="max-w-7xl mx-auto px-4">
<div className="flex justify-between h-16">
<div className="flex">
<div className="flex-shrink-0 flex items-center">
<h1 className="text-xl font-bold text-gray-800">
🛒 Grocery Tracker
</h1>
</div>
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
<Link
to="/"
className="text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium"
>
Dashboard
</Link>
<Link
to="/groceries"
className="text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium"
>
Groceries
</Link>
<Link
to="/shops"
className="text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium"
>
Shops
</Link>
<Link
to="/shopping-events"
className="text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium"
>
Shopping Events
</Link>
<Link
to="/add-purchase"
className="bg-blue-500 hover:bg-blue-700 text-white inline-flex items-center px-3 py-2 text-sm font-medium rounded-md"
>
Add New Event
</Link>
</div>
</div>
</div>
</div>
</nav>
{/* Main Content */}
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="min-h-screen bg-gray-100">
<Navigation />
<main className="container mx-auto py-8 px-4">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/groceries" element={<GroceryList />} />
<Route path="/products" element={<ProductList />} />
<Route path="/shops" element={<ShopList />} />
<Route path="/shopping-events" element={<ShoppingEventList />} />
<Route path="/shopping-events/:id/edit" element={<ShoppingEventForm />} />

View File

@@ -1,15 +1,15 @@
import React, { useState, useEffect } from 'react';
import { groceryApi } from '../services/api';
import { Grocery } from '../types';
import { productApi } from '../services/api';
import { Product } from '../types';
interface AddGroceryModalProps {
interface AddProductModalProps {
isOpen: boolean;
onClose: () => void;
onGroceryAdded: () => void;
editGrocery?: Grocery | null;
onProductAdded: () => void;
editProduct?: Product | null;
}
interface GroceryFormData {
interface ProductFormData {
name: string;
category: string;
organic: boolean;
@@ -17,8 +17,8 @@ interface GroceryFormData {
weight_unit: string;
}
const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGroceryAdded, editGrocery }) => {
const [formData, setFormData] = useState<GroceryFormData>({
const AddProductModal: React.FC<AddProductModalProps> = ({ isOpen, onClose, onProductAdded, editProduct }) => {
const [formData, setFormData] = useState<ProductFormData>({
name: '',
category: '',
organic: false,
@@ -37,16 +37,16 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
// Populate form when editing
useEffect(() => {
if (editGrocery) {
if (editProduct) {
setFormData({
name: editGrocery.name,
category: editGrocery.category,
organic: editGrocery.organic,
weight: editGrocery.weight,
weight_unit: editGrocery.weight_unit
name: editProduct.name,
category: editProduct.category,
organic: editProduct.organic,
weight: editProduct.weight,
weight_unit: editProduct.weight_unit
});
} else {
// Reset form for adding new grocery
// Reset form for adding new product
setFormData({
name: '',
category: '',
@@ -56,7 +56,7 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
});
}
setError('');
}, [editGrocery, isOpen]);
}, [editProduct, isOpen]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -69,17 +69,17 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
setLoading(true);
setError('');
const groceryData = {
const productData = {
...formData,
weight: formData.weight || undefined
};
if (editGrocery) {
// Update existing grocery
await groceryApi.update(editGrocery.id, groceryData);
if (editProduct) {
// Update existing product
await productApi.update(editProduct.id, productData);
} else {
// Create new grocery
await groceryApi.create(groceryData);
// Create new product
await productApi.create(productData);
}
// Reset form
@@ -91,11 +91,11 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
weight_unit: 'piece'
});
onGroceryAdded();
onProductAdded();
onClose();
} catch (err) {
setError(`Failed to ${editGrocery ? 'update' : 'add'} grocery. Please try again.`);
console.error(`Error ${editGrocery ? 'updating' : 'adding'} grocery:`, err);
setError(`Failed to ${editProduct ? 'update' : 'add'} product. Please try again.`);
console.error(`Error ${editProduct ? 'updating' : 'adding'} product:`, err);
} finally {
setLoading(false);
}
@@ -119,7 +119,7 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
<div className="mt-3">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">
{editGrocery ? 'Edit Grocery' : 'Add New Grocery'}
{editProduct ? 'Edit Product' : 'Add New Product'}
</h3>
<button
onClick={onClose}
@@ -236,8 +236,8 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading
? (editGrocery ? 'Updating...' : 'Adding...')
: (editGrocery ? 'Update Grocery' : 'Add Grocery')
? (editProduct ? 'Updating...' : 'Adding...')
: (editProduct ? 'Update Product' : 'Add Product')
}
</button>
</div>
@@ -248,4 +248,4 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
);
};
export default AddGroceryModal;
export default AddProductModal;

View File

@@ -32,7 +32,7 @@ const Dashboard: React.FC = () => {
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600">Welcome to your grocery tracker!</p>
<p className="text-gray-600">Welcome to your product tracker!</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
@@ -102,7 +102,7 @@ const Dashboard: React.FC = () => {
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<button
onClick={() => navigate('/add-purchase')}
onClick={() => navigate('/shopping-events')}
className="flex items-center p-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="p-2 bg-blue-100 rounded-md mr-3">
@@ -117,7 +117,7 @@ const Dashboard: React.FC = () => {
</button>
<button
onClick={() => navigate('/groceries?add=true')}
onClick={() => navigate('/products?add=true')}
className="flex items-center p-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="p-2 bg-green-100 rounded-md mr-3">
@@ -126,8 +126,8 @@ const Dashboard: React.FC = () => {
</svg>
</div>
<div>
<p className="font-medium text-gray-900">Add Grocery</p>
<p className="text-sm text-gray-600">Add a new grocery item</p>
<p className="font-medium text-gray-900">Add Product</p>
<p className="text-sm text-gray-600">Add a new product item</p>
</div>
</button>
@@ -181,9 +181,9 @@ const Dashboard: React.FC = () => {
<p className="text-sm text-gray-600 mt-1">
{new Date(event.date).toLocaleDateString()}
</p>
{event.groceries.length > 0 && (
{event.products.length > 0 && (
<p className="text-sm text-gray-500 mt-1">
{event.groceries.length} item{event.groceries.length !== 1 ? 's' : ''}
{event.products.length} item{event.products.length !== 1 ? 's' : ''}
</p>
)}
</div>

View File

@@ -1,22 +1,22 @@
import React, { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Grocery } from '../types';
import { groceryApi } from '../services/api';
import AddGroceryModal from './AddGroceryModal';
import { Product } from '../types';
import { productApi } from '../services/api';
import AddProductModal from './AddProductModal';
import ConfirmDeleteModal from './ConfirmDeleteModal';
const GroceryList: React.FC = () => {
const ProductList: React.FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
const [groceries, setGroceries] = useState<Grocery[]>([]);
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingGrocery, setEditingGrocery] = useState<Grocery | null>(null);
const [deletingGrocery, setDeletingGrocery] = useState<Grocery | null>(null);
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
const [deletingProduct, setDeletingProduct] = useState<Product | null>(null);
const [deleteLoading, setDeleteLoading] = useState(false);
useEffect(() => {
fetchGroceries();
fetchProducts();
// Check if we should auto-open the modal
if (searchParams.get('add') === 'true') {
@@ -26,55 +26,55 @@ const GroceryList: React.FC = () => {
}
}, [searchParams, setSearchParams]);
const fetchGroceries = async () => {
const fetchProducts = async () => {
try {
setLoading(true);
const response = await groceryApi.getAll();
setGroceries(response.data);
const response = await productApi.getAll();
setProducts(response.data);
} catch (err) {
setError('Failed to fetch groceries');
console.error('Error fetching groceries:', err);
setError('Failed to fetch products');
console.error('Error fetching products:', err);
} finally {
setLoading(false);
}
};
const handleEdit = (grocery: Grocery) => {
setEditingGrocery(grocery);
const handleEdit = (product: Product) => {
setEditingProduct(product);
setIsModalOpen(true);
};
const handleDelete = (grocery: Grocery) => {
setDeletingGrocery(grocery);
const handleDelete = (product: Product) => {
setDeletingProduct(product);
};
const confirmDelete = async () => {
if (!deletingGrocery) return;
if (!deletingProduct) return;
try {
setDeleteLoading(true);
await groceryApi.delete(deletingGrocery.id);
setDeletingGrocery(null);
fetchGroceries(); // Refresh the list
await productApi.delete(deletingProduct.id);
setDeletingProduct(null);
fetchProducts(); // Refresh the list
} catch (err) {
console.error('Error deleting grocery:', err);
setError('Failed to delete grocery. Please try again.');
console.error('Error deleting product:', err);
setError('Failed to delete product. Please try again.');
} finally {
setDeleteLoading(false);
}
};
const handleGroceryAdded = () => {
fetchGroceries(); // Refresh the list
const handleProductAdded = () => {
fetchProducts(); // Refresh the list
};
const handleCloseModal = () => {
setIsModalOpen(false);
setEditingGrocery(null);
setEditingProduct(null);
};
const handleCloseDeleteModal = () => {
setDeletingGrocery(null);
setDeletingProduct(null);
};
if (loading) {
@@ -88,15 +88,15 @@ const GroceryList: React.FC = () => {
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-900">Groceries</h1>
<h1 className="text-2xl font-bold text-gray-900">Products</h1>
<button
onClick={() => {
setEditingGrocery(null);
setEditingProduct(null);
setIsModalOpen(true);
}}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Add New Grocery
Add New Product
</button>
</div>
@@ -107,13 +107,13 @@ const GroceryList: React.FC = () => {
)}
<div className="bg-white shadow rounded-lg overflow-hidden">
{groceries.length === 0 ? (
{products.length === 0 ? (
<div className="text-center py-12">
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 48 48">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">No groceries</h3>
<p className="mt-1 text-sm text-gray-500">Get started by adding your first grocery item.</p>
<h3 className="mt-2 text-sm font-medium text-gray-900">No products</h3>
<p className="mt-1 text-sm text-gray-500">Get started by adding your first product item.</p>
</div>
) : (
<table className="min-w-full divide-y divide-gray-200">
@@ -137,39 +137,39 @@ const GroceryList: React.FC = () => {
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{groceries.map((grocery) => (
<tr key={grocery.id} className="hover:bg-gray-50">
{products.map((product) => (
<tr key={product.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{grocery.name} {grocery.organic ? '🌱' : ''}
{product.name} {product.organic ? '🌱' : ''}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
{grocery.category}
{product.category}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{grocery.weight ? `${grocery.weight}${grocery.weight_unit}` : '-'}
{product.weight ? `${product.weight}${product.weight_unit}` : '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
grocery.organic
product.organic
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{grocery.organic ? 'Organic' : 'Conventional'}
{product.organic ? 'Organic' : 'Conventional'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button
onClick={() => handleEdit(grocery)}
onClick={() => handleEdit(product)}
className="text-indigo-600 hover:text-indigo-900 mr-3"
>
Edit
</button>
<button
onClick={() => handleDelete(grocery)}
onClick={() => handleDelete(product)}
className="text-red-600 hover:text-red-900"
>
Delete
@@ -182,23 +182,23 @@ const GroceryList: React.FC = () => {
)}
</div>
<AddGroceryModal
<AddProductModal
isOpen={isModalOpen}
onClose={handleCloseModal}
onGroceryAdded={handleGroceryAdded}
editGrocery={editingGrocery}
onProductAdded={handleProductAdded}
editProduct={editingProduct}
/>
<ConfirmDeleteModal
isOpen={!!deletingGrocery}
isOpen={!!deletingProduct}
onClose={handleCloseDeleteModal}
onConfirm={confirmDelete}
title="Delete Grocery"
message={`Are you sure you want to delete "${deletingGrocery?.name}"? This action cannot be undone.`}
title="Delete Product"
message={`Are you sure you want to delete "${deletingProduct?.name}"? This action cannot be undone.`}
isLoading={deleteLoading}
/>
</div>
);
};
export default GroceryList;
export default ProductList;

View File

@@ -1,13 +1,13 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Shop, Grocery, ShoppingEventCreate, GroceryInEvent } from '../types';
import { shopApi, groceryApi, shoppingEventApi } from '../services/api';
import { Shop, Product, ShoppingEventCreate, ProductInEvent } from '../types';
import { shopApi, productApi, shoppingEventApi } from '../services/api';
const ShoppingEventForm: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [shops, setShops] = useState<Shop[]>([]);
const [groceries, setGroceries] = useState<Grocery[]>([]);
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(false);
const [loadingEvent, setLoadingEvent] = useState(false);
const [message, setMessage] = useState('');
@@ -19,12 +19,12 @@ const ShoppingEventForm: React.FC = () => {
date: new Date().toISOString().split('T')[0],
total_amount: undefined,
notes: '',
groceries: []
products: []
});
const [selectedGroceries, setSelectedGroceries] = useState<GroceryInEvent[]>([]);
const [newGroceryItem, setNewGroceryItem] = useState<GroceryInEvent>({
grocery_id: 0,
const [selectedProducts, setSelectedProducts] = useState<ProductInEvent[]>([]);
const [newProductItem, setNewProductItem] = useState<ProductInEvent>({
product_id: 0,
amount: 1,
price: 0
});
@@ -32,28 +32,28 @@ const ShoppingEventForm: React.FC = () => {
useEffect(() => {
fetchShops();
fetchGroceries();
fetchProducts();
if (isEditMode && id) {
fetchShoppingEvent(parseInt(id));
}
}, [id, isEditMode]);
// Calculate total amount from selected groceries
const calculateTotal = (groceries: GroceryInEvent[]): number => {
const total = groceries.reduce((total, item) => total + (item.amount * item.price), 0);
// Calculate total amount from selected products
const calculateTotal = (products: ProductInEvent[]): number => {
const total = products.reduce((total, item) => total + (item.amount * item.price), 0);
return Math.round(total * 100) / 100; // Round to 2 decimal places to avoid floating-point errors
};
// Update total amount whenever selectedGroceries changes
// Update total amount whenever selectedProducts changes
useEffect(() => {
if (autoCalculate) {
const calculatedTotal = calculateTotal(selectedGroceries);
const calculatedTotal = calculateTotal(selectedProducts);
setFormData(prev => ({
...prev,
total_amount: calculatedTotal > 0 ? calculatedTotal : undefined
}));
}
}, [selectedGroceries, autoCalculate]);
}, [selectedProducts, autoCalculate]);
const fetchShops = async () => {
try {
@@ -64,12 +64,12 @@ const ShoppingEventForm: React.FC = () => {
}
};
const fetchGroceries = async () => {
const fetchProducts = async () => {
try {
const response = await groceryApi.getAll();
setGroceries(response.data);
const response = await productApi.getAll();
setProducts(response.data);
} catch (error) {
console.error('Error fetching groceries:', error);
console.error('Error fetching products:', error);
}
};
@@ -86,15 +86,15 @@ const ShoppingEventForm: React.FC = () => {
formattedDate = event.date.split('T')[0];
}
// Map groceries to the format we need
const mappedGroceries = event.groceries.map(g => ({
grocery_id: g.id,
amount: g.amount,
price: g.price
// Map products to the format we need
const mappedProducts = event.products.map(p => ({
product_id: p.id,
amount: p.amount,
price: p.price
}));
// Calculate the sum of all groceries
const calculatedTotal = calculateTotal(mappedGroceries);
// Calculate the sum of all products
const calculatedTotal = calculateTotal(mappedProducts);
// Check if existing total matches calculated total (with small tolerance for floating point)
const existingTotal = event.total_amount || 0;
@@ -105,10 +105,10 @@ const ShoppingEventForm: React.FC = () => {
date: formattedDate,
total_amount: event.total_amount,
notes: event.notes || '',
groceries: []
products: []
});
setSelectedGroceries(mappedGroceries);
setSelectedProducts(mappedProducts);
setAutoCalculate(totalMatches); // Enable auto-calc if totals match, disable if they don't
} catch (error) {
console.error('Error fetching shopping event:', error);
@@ -118,27 +118,27 @@ const ShoppingEventForm: React.FC = () => {
}
};
const addGroceryToEvent = () => {
if (newGroceryItem.grocery_id > 0 && newGroceryItem.amount > 0 && newGroceryItem.price >= 0) {
setSelectedGroceries([...selectedGroceries, { ...newGroceryItem }]);
setNewGroceryItem({ grocery_id: 0, amount: 1, price: 0 });
const addProductToEvent = () => {
if (newProductItem.product_id > 0 && newProductItem.amount > 0 && newProductItem.price >= 0) {
setSelectedProducts([...selectedProducts, { ...newProductItem }]);
setNewProductItem({ product_id: 0, amount: 1, price: 0 });
}
};
const removeGroceryFromEvent = (index: number) => {
setSelectedGroceries(selectedGroceries.filter((_, i) => i !== index));
const removeProductFromEvent = (index: number) => {
setSelectedProducts(selectedProducts.filter((_, i) => i !== index));
};
const editGroceryFromEvent = (index: number) => {
const groceryToEdit = selectedGroceries[index];
// Load the grocery data into the input fields
setNewGroceryItem({
grocery_id: groceryToEdit.grocery_id,
amount: groceryToEdit.amount,
price: groceryToEdit.price
const editProductFromEvent = (index: number) => {
const productToEdit = selectedProducts[index];
// Load the product data into the input fields
setNewProductItem({
product_id: productToEdit.product_id,
amount: productToEdit.amount,
price: productToEdit.price
});
// Remove the item from the selected list
setSelectedGroceries(selectedGroceries.filter((_, i) => i !== index));
setSelectedProducts(selectedProducts.filter((_, i) => i !== index));
};
const handleSubmit = async (e: React.FormEvent) => {
@@ -149,7 +149,7 @@ const ShoppingEventForm: React.FC = () => {
try {
const eventData = {
...formData,
groceries: selectedGroceries
products: selectedProducts
};
if (isEditMode) {
@@ -173,9 +173,9 @@ const ShoppingEventForm: React.FC = () => {
date: new Date().toISOString().split('T')[0],
total_amount: undefined,
notes: '',
groceries: []
products: []
});
setSelectedGroceries([]);
setSelectedProducts([]);
}
} catch (error) {
console.error('Full error object:', error);
@@ -185,13 +185,13 @@ const ShoppingEventForm: React.FC = () => {
}
};
const getGroceryName = (id: number) => {
const grocery = groceries.find(g => g.id === id);
if (!grocery) return 'Unknown';
const getProductName = (id: number) => {
const product = products.find(p => p.id === id);
if (!product) return 'Unknown';
const weightInfo = grocery.weight ? `${grocery.weight}${grocery.weight_unit}` : grocery.weight_unit;
const organicEmoji = grocery.organic ? ' 🌱' : '';
return `${grocery.name}${organicEmoji} ${weightInfo}`;
const weightInfo = product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit;
const organicEmoji = product.organic ? ' 🌱' : '';
return `${product.name}${organicEmoji} ${weightInfo}`;
};
if (loadingEvent) {
@@ -265,25 +265,25 @@ const ShoppingEventForm: React.FC = () => {
/>
</div>
{/* Add Groceries Section */}
{/* Add Products Section */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Add Groceries
Add Products
</label>
<div className="flex space-x-2 mb-4">
<div className="flex-1">
<label className="block text-xs font-medium text-gray-700 mb-1">
Grocery
Product
</label>
<select
value={newGroceryItem.grocery_id}
onChange={(e) => setNewGroceryItem({...newGroceryItem, grocery_id: parseInt(e.target.value)})}
value={newProductItem.product_id}
onChange={(e) => setNewProductItem({...newProductItem, product_id: parseInt(e.target.value)})}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value={0}>Select a grocery</option>
{groceries.map(grocery => (
<option key={grocery.id} value={grocery.id}>
{grocery.name}{grocery.organic ? '🌱' : ''} ({grocery.category}) {grocery.weight ? `${grocery.weight}${grocery.weight_unit}` : grocery.weight_unit}
<option value={0}>Select a product</option>
{products.map(product => (
<option key={product.id} value={product.id}>
{product.name}{product.organic ? '🌱' : ''} ({product.category}) {product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit}
</option>
))}
</select>
@@ -297,8 +297,8 @@ const ShoppingEventForm: React.FC = () => {
step="1"
min="1"
placeholder="1"
value={newGroceryItem.amount}
onChange={(e) => setNewGroceryItem({...newGroceryItem, amount: parseFloat(e.target.value)})}
value={newProductItem.amount}
onChange={(e) => setNewProductItem({...newProductItem, amount: parseFloat(e.target.value)})}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
@@ -311,15 +311,15 @@ const ShoppingEventForm: React.FC = () => {
step="0.01"
min="0"
placeholder="0.00"
value={newGroceryItem.price}
onChange={(e) => setNewGroceryItem({...newGroceryItem, price: parseFloat(e.target.value)})}
value={newProductItem.price}
onChange={(e) => setNewProductItem({...newProductItem, price: parseFloat(e.target.value)})}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex items-end">
<button
type="button"
onClick={addGroceryToEvent}
onClick={addProductToEvent}
className="bg-green-500 hover:bg-green-700 text-white px-4 py-2 rounded-md"
>
Add
@@ -327,15 +327,15 @@ const ShoppingEventForm: React.FC = () => {
</div>
</div>
{/* Selected Groceries List */}
{selectedGroceries.length > 0 && (
{/* Selected Products List */}
{selectedProducts.length > 0 && (
<div className="bg-gray-50 rounded-md p-4">
<h4 className="font-medium text-gray-700 mb-2">Selected Items:</h4>
{selectedGroceries.map((item, index) => (
{selectedProducts.map((item, index) => (
<div key={index} className="flex justify-between items-center py-2 border-b last:border-b-0">
<div className="flex-1">
<div className="text-sm text-gray-900">
{getGroceryName(item.grocery_id)}
{getProductName(item.product_id)}
</div>
<div className="text-xs text-gray-600">
{item.amount} × ${item.price.toFixed(2)} = ${(item.amount * item.price).toFixed(2)}
@@ -344,14 +344,14 @@ const ShoppingEventForm: React.FC = () => {
<div className="flex space-x-2">
<button
type="button"
onClick={() => editGroceryFromEvent(index)}
onClick={() => editProductFromEvent(index)}
className="text-blue-500 hover:text-blue-700"
>
Edit
</button>
<button
type="button"
onClick={() => removeGroceryFromEvent(index)}
onClick={() => removeProductFromEvent(index)}
className="text-red-500 hover:text-red-700"
>
Remove
@@ -431,7 +431,7 @@ const ShoppingEventForm: React.FC = () => {
)}
<button
type="submit"
disabled={loading || formData.shop_id === 0 || selectedGroceries.length === 0}
disabled={loading || formData.shop_id === 0 || selectedProducts.length === 0}
className={`px-4 py-2 text-sm font-medium text-white rounded-md disabled:opacity-50 disabled:cursor-not-allowed ${
isEditMode
? 'bg-blue-600 hover:bg-blue-700'

View File

@@ -109,17 +109,17 @@ const ShoppingEventList: React.FC = () => {
</div>
</div>
{event.groceries.length > 0 && (
{event.products.length > 0 && (
<div className="mb-4">
<h4 className="text-sm font-medium text-gray-700 mb-2">Items Purchased:</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
{event.groceries.map((grocery) => (
<div key={grocery.id} className="bg-gray-50 rounded px-3 py-2">
{event.products.map((product) => (
<div key={product.id} className="bg-gray-50 rounded px-3 py-2">
<div className="text-sm text-gray-900">
{grocery.name} {grocery.organic ? '🌱' : ''}
{product.name} {product.organic ? '🌱' : ''}
</div>
<div className="text-xs text-gray-600">
{grocery.amount} × ${grocery.price.toFixed(2)} = ${(grocery.amount * grocery.price).toFixed(2)}
{product.amount} × ${product.price.toFixed(2)} = ${(product.amount * product.price).toFixed(2)}
</div>
</div>
))}

View File

@@ -1,23 +1,34 @@
import axios from 'axios';
import { Grocery, GroceryCreate, Shop, ShopCreate, ShoppingEvent, ShoppingEventCreate } from '../types';
import { Product, ProductCreate, Shop, ShopCreate, ShoppingEvent, ShoppingEventCreate } from '../types';
const BASE_URL = 'http://localhost:8000';
const API_BASE_URL = 'http://localhost:8000';
const api = axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
const api = {
get: <T>(url: string): Promise<{ data: T }> =>
fetch(`${API_BASE_URL}${url}`).then(res => res.json()).then(data => ({ data })),
post: <T>(url: string, body: any): Promise<{ data: T }> =>
fetch(`${API_BASE_URL}${url}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(res => res.json()).then(data => ({ data })),
put: <T>(url: string, body: any): Promise<{ data: T }> =>
fetch(`${API_BASE_URL}${url}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(res => res.json()).then(data => ({ data })),
delete: (url: string): Promise<void> =>
fetch(`${API_BASE_URL}${url}`, { method: 'DELETE' }).then(() => {}),
};
// Grocery API functions
export const groceryApi = {
getAll: () => api.get<Grocery[]>('/groceries/'),
getById: (id: number) => api.get<Grocery>(`/groceries/${id}`),
create: (grocery: GroceryCreate) => api.post<Grocery>('/groceries/', grocery),
update: (id: number, grocery: Partial<GroceryCreate>) =>
api.put<Grocery>(`/groceries/${id}`, grocery),
delete: (id: number) => api.delete(`/groceries/${id}`),
// Product API functions
export const productApi = {
getAll: () => api.get<Product[]>('/products/'),
getById: (id: number) => api.get<Product>(`/products/${id}`),
create: (product: ProductCreate) => api.post<Product>('/products/', product),
update: (id: number, product: Partial<ProductCreate>) =>
api.put<Product>(`/products/${id}`, product),
delete: (id: number) => api.delete(`/products/${id}`),
};
// Shop API functions
@@ -25,7 +36,7 @@ export const shopApi = {
getAll: () => api.get<Shop[]>('/shops/'),
getById: (id: number) => api.get<Shop>(`/shops/${id}`),
create: (shop: ShopCreate) => api.post<Shop>('/shops/', shop),
update: (id: number, shop: Partial<ShopCreate>) =>
update: (id: number, shop: Partial<ShopCreate>) =>
api.put<Shop>(`/shops/${id}`, shop),
delete: (id: number) => api.delete(`/shops/${id}`),
};
@@ -34,9 +45,8 @@ export const shopApi = {
export const shoppingEventApi = {
getAll: () => api.get<ShoppingEvent[]>('/shopping-events/'),
getById: (id: number) => api.get<ShoppingEvent>(`/shopping-events/${id}`),
create: (event: ShoppingEventCreate) =>
api.post<ShoppingEvent>('/shopping-events/', event),
update: (id: number, event: ShoppingEventCreate) =>
create: (event: ShoppingEventCreate) => api.post<ShoppingEvent>('/shopping-events/', event),
update: (id: number, event: ShoppingEventCreate) =>
api.put<ShoppingEvent>(`/shopping-events/${id}`, event),
delete: (id: number) => api.delete(`/shopping-events/${id}`),
};

View File

@@ -1,4 +1,4 @@
export interface Grocery {
export interface Product {
id: number;
name: string;
category: string;
@@ -9,7 +9,7 @@ export interface Grocery {
updated_at?: string;
}
export interface GroceryCreate {
export interface ProductCreate {
name: string;
category: string;
organic: boolean;
@@ -32,13 +32,13 @@ export interface ShopCreate {
address?: string | null;
}
export interface GroceryInEvent {
grocery_id: number;
export interface ProductInEvent {
product_id: number;
amount: number;
price: number;
}
export interface GroceryWithEventData {
export interface ProductWithEventData {
id: number;
name: string;
category: string;
@@ -58,7 +58,7 @@ export interface ShoppingEvent {
created_at: string;
updated_at?: string;
shop: Shop;
groceries: GroceryWithEventData[];
products: ProductWithEventData[];
}
export interface ShoppingEventCreate {
@@ -66,7 +66,7 @@ export interface ShoppingEventCreate {
date?: string;
total_amount?: number;
notes?: string;
groceries: GroceryInEvent[];
products: ProductInEvent[];
}
export interface CategoryStats {