rename grocery to product
This commit is contained in:
@@ -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 />} />
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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}`),
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user