diff --git a/QUICKSTART.md b/QUICKSTART.md index e6eb8d1..1c56d2f 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -1,6 +1,6 @@ # ๐Ÿš€ Quick Start Guide -Get your Grocery Tracker up and running in minutes! +Get your Product Tracker up and running in minutes! ## Prerequisites @@ -62,8 +62,8 @@ This will: ## First Steps -1. **Add a Shop**: Go to "Shops" and add your first grocery store -2. **Add Groceries**: Go to "Groceries" and add some items +1. **Add a Shop**: Go to "Shops" and add your first store +2. **Add Products**: Go to "Products" and add some items 3. **Record a Purchase**: Use "Add Purchase" to record your shopping ## Troubleshooting @@ -85,8 +85,8 @@ This will: โœ… **Complete Backend**: FastAPI with SQLAlchemy and SQLite โœ… **Modern Frontend**: React with TypeScript and Tailwind CSS -โœ… **Database Models**: Groceries, Shops, Shopping Events +โœ… **Database Models**: Products, Shops, Shopping Events โœ… **API Documentation**: Automatic Swagger docs โœ… **Beautiful UI**: Responsive design with modern components -Happy grocery tracking! ๐Ÿ›’ \ No newline at end of file +Happy product tracking! ๐Ÿ›’ \ No newline at end of file diff --git a/README.md b/README.md index 63f78cb..b89b7c3 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# Grocery Tracker +# Product Tracker -A web application for tracking grocery prices and shopping events. Built with FastAPI (Python) backend and React (TypeScript) frontend. +A web application for tracking product prices and shopping events. Built with FastAPI (Python) backend and React (TypeScript) frontend. ## Features -- **Grocery Management**: Add, edit, and track grocery items with prices, categories, and organic status +- **Product Management**: Add, edit, and track product items with prices, categories, and organic status - **Shop Management**: Manage different shops with locations -- **Shopping Events**: Record purchases with multiple groceries and amounts +- **Shopping Events**: Record purchases with multiple products and amounts - **Price Tracking**: Monitor price changes over time - **Modern UI**: Clean, responsive interface built with React and Tailwind CSS @@ -42,9 +42,9 @@ A web application for tracking grocery prices and shopping events. Built with Fa ### Core Entities -#### Groceries (`groceries` table) +#### Products (`products` table) - `id`: Integer, Primary key, Auto-increment -- `name`: String, Grocery name (indexed, required) +- `name`: String, Product name (indexed, required) - `category`: String, Food category (required) - `organic`: Boolean, Organic flag (default: false) - `weight`: Float, Weight/volume (optional) @@ -72,11 +72,11 @@ A web application for tracking grocery prices and shopping events. Built with Fa ### Association Table -#### Shopping Event Groceries (`shopping_event_groceries` table) -Many-to-many relationship between shopping events and groceries with additional data: +#### Shopping Event Products (`shopping_event_products` table) +Many-to-many relationship between shopping events and products with additional data: - `id`: Integer, Primary key, Auto-increment - `shopping_event_id`: Integer, Foreign key to shopping_events (required) -- `grocery_id`: Integer, Foreign key to groceries (required) +- `product_id`: Integer, Foreign key to products (required) - `amount`: Float, Quantity purchased in this event (required, > 0) - `price`: Float, Price at time of purchase (required, โ‰ฅ 0) @@ -84,10 +84,10 @@ Many-to-many relationship between shopping events and groceries with additional ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Shops โ”‚ โ”‚ Shopping Event Groceries โ”‚ โ”‚ Groceries โ”‚ +โ”‚ Shops โ”‚ โ”‚ Shopping Event Products โ”‚ โ”‚ Products โ”‚ โ”‚ โ”‚ โ”‚ (Association Table) โ”‚ โ”‚ โ”‚ โ”‚ โ€ข id โ”‚ โ†โ”€โ”€โ”€โ”€โ”€โ”€โ†’โ”‚ โ€ข shopping_event_id โ”‚ โ†โ”€โ”€โ”€โ”€โ”€โ”€โ†’โ”‚ โ€ข id โ”‚ -โ”‚ โ€ข name โ”‚ 1:N โ”‚ โ€ข grocery_id โ”‚ N:M โ”‚ โ€ข name โ”‚ +โ”‚ โ€ข name โ”‚ 1:N โ”‚ โ€ข product_id โ”‚ N:M โ”‚ โ€ข name โ”‚ โ”‚ โ€ข city โ”‚ โ”‚ โ€ข amount โ”‚ โ”‚ โ€ข category โ”‚ โ”‚ โ€ข address โ”‚ โ”‚ โ€ข price โ”‚ โ”‚ โ€ข organic โ”‚ โ”‚ โ€ข created_at โ”‚ โ”‚ โ”‚ โ”‚ โ€ข weight โ”‚ @@ -110,7 +110,7 @@ Many-to-many relationship between shopping events and groceries with additional ### Key Features -- **Price History**: Each grocery purchase stores the price at that time, enabling price tracking +- **Price History**: Each product purchase stores the price at that time, enabling price tracking - **Flexible Quantities**: Support for decimal amounts (e.g., 1.5 kg of apples) - **Auto-calculation**: Total amount can be automatically calculated from individual items - **Free Items**: Supports items with price 0 (samples, promotions, etc.) @@ -146,7 +146,7 @@ Many-to-many relationship between shopping events and groceries with additional 4. **Setup database:** ```bash # Create PostgreSQL database - createdb grocery_tracker + createdb product_tracker # Copy environment variables cp env.example .env @@ -189,12 +189,12 @@ Many-to-many relationship between shopping events and groceries with additional ## API Endpoints -### Groceries -- `GET /groceries/` - List all groceries -- `POST /groceries/` - Create new grocery -- `GET /groceries/{id}` - Get specific grocery -- `PUT /groceries/{id}` - Update grocery -- `DELETE /groceries/{id}` - Delete grocery +### Products +- `GET /products/` - List all products +- `POST /products/` - Create new product +- `GET /products/{id}` - Get specific product +- `PUT /products/{id}` - Update product +- `DELETE /products/{id}` - Delete product ### Shops - `GET /shops/` - List all shops @@ -212,8 +212,8 @@ Many-to-many relationship between shopping events and groceries with additional ## Usage -1. **Add Shops**: Start by adding shops where you buy groceries -2. **Add Groceries**: Create grocery items with prices and categories +1. **Add Shops**: Start by adding shops where you buy products +2. **Add Products**: Create product items with prices and categories 3. **Record Purchases**: Use the "Add Purchase" form to record shopping events 4. **Track Prices**: Monitor how prices change over time 5. **View Statistics**: Analyze spending patterns by category and shop @@ -252,8 +252,8 @@ services: db: image: postgres:15 environment: - POSTGRES_DB: grocery_tracker - POSTGRES_USER: grocery_user + POSTGRES_DB: product_tracker + POSTGRES_USER: product_user POSTGRES_PASSWORD: your_password volumes: - postgres_data:/var/lib/postgresql/data @@ -267,7 +267,7 @@ services: depends_on: - db environment: - DATABASE_URL: postgresql://grocery_user:your_password@db:5432/grocery_tracker + DATABASE_URL: postgresql://product_user:your_password@db:5432/product_tracker frontend: build: ./frontend diff --git a/backend/TEST_DATA_README.md b/backend/TEST_DATA_README.md index 38a7a23..614e6a3 100644 --- a/backend/TEST_DATA_README.md +++ b/backend/TEST_DATA_README.md @@ -1,12 +1,12 @@ # Test Data Scripts Documentation -This directory contains scripts for creating and managing test data for the Grocery Tracker application. +This directory contains scripts for creating and managing test data for the Product Tracker application. ## Scripts Overview ### 1. `create_test_data.py` - Comprehensive Test Data Generator -Creates realistic test data including shops, groceries, and shopping events. +Creates realistic test data including shops, products, and shopping events. #### Basic Usage @@ -32,7 +32,7 @@ python create_test_data.py --dry-run | `--days N` | Number of days back to generate events | 90 | | `--url URL` | API base URL | http://localhost:8000 | | `--shops-only` | Create only shops | False | -| `--groceries-only` | Create only groceries | False | +| `--products-only` | Create only products | False | | `--events-only` | Create only shopping events (requires existing data) | False | | `--verbose`, `-v` | Verbose output with detailed progress | False | | `--dry-run` | Show what would be created without creating it | False | @@ -43,10 +43,10 @@ python create_test_data.py --dry-run # Create only shops python create_test_data.py --shops-only -# Create only groceries -python create_test_data.py --groceries-only +# Create only products +python create_test_data.py --products-only -# Create 100 shopping events using existing shops and groceries +# Create 100 shopping events using existing shops and products python create_test_data.py --events-only --events 100 # Create test data for the past 6 months with verbose output @@ -74,14 +74,14 @@ python cleanup_test_data.py - **Safeway** (San Francisco) - **Trader Joe's** (Berkeley) - **Berkeley Bowl** (Berkeley) -- **Rainbow Grocery** (San Francisco) +- **Rainbow Product** (San Francisco) - **Mollie Stone's Market** (Palo Alto) - **Costco Wholesale** (San Mateo) - **Target** (Mountain View) - **Sprouts Farmers Market** (Sunnyvale) - **Lucky Supermarket** (San Jose) -### Groceries (50+ items across 8 categories) +### Products (50+ items across 8 categories) | Category | Items | Organic Options | |----------|-------|-----------------| @@ -190,7 +190,7 @@ After running the test data scripts: The test data is designed to showcase all application features: - Multiple shops and locations -- Diverse grocery categories +- Diverse product categories - Realistic shopping patterns - Price variations and organic options - Historical data for analytics @@ -202,7 +202,7 @@ The test data is designed to showcase all application features: You can modify the data arrays in `create_test_data.py` to create custom test scenarios: - Add more shops for specific regions -- Include specialty grocery categories +- Include specialty product categories - Adjust price ranges for different markets - Create seasonal shopping patterns diff --git a/backend/cleanup_test_data.py b/backend/cleanup_test_data.py index e3ceb53..90004b2 100644 --- a/backend/cleanup_test_data.py +++ b/backend/cleanup_test_data.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -Script to clean up all test data from the Grocery Tracker application. -This will delete all shopping events, groceries, and shops. +Script to clean up all test data from the Product Tracker application. +This will delete all shopping events, products, and shops. """ import requests @@ -42,37 +42,37 @@ def delete_all_shopping_events() -> int: print(f" โŒ Error fetching shopping events: {e}") return 0 -def delete_all_groceries() -> int: - """Delete all groceries and return the count of deleted groceries.""" - print("๐Ÿฅฌ Deleting all groceries...") +def delete_all_products() -> int: + """Delete all products and return the count of deleted products.""" + print("๐Ÿฅฌ Deleting all products...") try: - # Get all groceries - response = requests.get(f"{BASE_URL}/groceries/") + # Get all products + response = requests.get(f"{BASE_URL}/products/") if response.status_code != 200: - print(f" โŒ Failed to fetch groceries: {response.status_code}") + print(f" โŒ Failed to fetch products: {response.status_code}") return 0 - groceries = response.json() + products = response.json() deleted_count = 0 - for grocery in groceries: + for product in products: try: - delete_response = requests.delete(f"{BASE_URL}/groceries/{grocery['id']}") + delete_response = requests.delete(f"{BASE_URL}/products/{product['id']}") if delete_response.status_code == 200: deleted_count += 1 - organic_label = "๐ŸŒฑ" if grocery['organic'] else "๐ŸŒพ" - print(f" โœ… Deleted grocery: {organic_label} {grocery['name']}") + organic_label = "๐ŸŒฑ" if product['organic'] else "๐ŸŒพ" + print(f" โœ… Deleted product: {organic_label} {product['name']}") else: - print(f" โŒ Failed to delete grocery {grocery['name']}: {delete_response.status_code}") + print(f" โŒ Failed to delete product {product['name']}: {delete_response.status_code}") except Exception as e: - print(f" โŒ Error deleting grocery {grocery['name']}: {e}") + print(f" โŒ Error deleting product {product['name']}: {e}") - print(f" ๐Ÿ“Š Deleted {deleted_count} groceries total\n") + print(f" ๐Ÿ“Š Deleted {deleted_count} products total\n") return deleted_count except Exception as e: - print(f" โŒ Error fetching groceries: {e}") + print(f" โŒ Error fetching products: {e}") return 0 def delete_all_shops() -> int: @@ -115,15 +115,15 @@ def get_current_data_summary(): try: # Get counts shops_response = requests.get(f"{BASE_URL}/shops/") - groceries_response = requests.get(f"{BASE_URL}/groceries/") + products_response = requests.get(f"{BASE_URL}/products/") events_response = requests.get(f"{BASE_URL}/shopping-events/") shops_count = len(shops_response.json()) if shops_response.status_code == 200 else 0 - groceries_count = len(groceries_response.json()) if groceries_response.status_code == 200 else 0 + products_count = len(products_response.json()) if products_response.status_code == 200 else 0 events_count = len(events_response.json()) if events_response.status_code == 200 else 0 print(f"๐Ÿช Shops: {shops_count}") - print(f"๐Ÿฅฌ Groceries: {groceries_count}") + print(f"๐Ÿฅฌ Products: {products_count}") print(f"๐Ÿ›’ Shopping Events: {events_count}") if events_count > 0 and events_response.status_code == 200: @@ -132,7 +132,7 @@ def get_current_data_summary(): print(f"๐Ÿ’ฐ Total spent: ${total_spent:.2f}") print() - return shops_count, groceries_count, events_count + return shops_count, products_count, events_count except Exception as e: print(f"โŒ Error getting data summary: {e}") @@ -140,9 +140,9 @@ def get_current_data_summary(): def main(): """Main function to clean up all test data.""" - print("๐Ÿงน GROCERY TRACKER DATA CLEANUP") + print("๐Ÿงน PRODUCT TRACKER DATA CLEANUP") print("=" * 40) - print("This script will delete ALL data from the Grocery Tracker app.") + print("This script will delete ALL data from the Product Tracker app.") print("Make sure the backend server is running on http://localhost:8000\n") try: @@ -155,9 +155,9 @@ def main(): print("โœ… Connected to API server\n") # Show current data - shops_count, groceries_count, events_count = get_current_data_summary() + shops_count, products_count, events_count = get_current_data_summary() - if shops_count == 0 and groceries_count == 0 and events_count == 0: + if shops_count == 0 and products_count == 0 and events_count == 0: print("โœ… Database is already empty. Nothing to clean up!") return @@ -171,19 +171,19 @@ def main(): print("\n๐Ÿงน Starting cleanup process...\n") - # Delete in order: events -> groceries -> shops + # Delete in order: events -> products -> shops # (due to foreign key constraints) deleted_events = delete_all_shopping_events() - deleted_groceries = delete_all_groceries() + deleted_products = delete_all_products() deleted_shops = delete_all_shops() # Final summary print("๐Ÿ“‹ CLEANUP SUMMARY") print("=" * 30) print(f"๐Ÿ›’ Shopping Events deleted: {deleted_events}") - print(f"๐Ÿฅฌ Groceries deleted: {deleted_groceries}") + print(f"๐Ÿฅฌ Products deleted: {deleted_products}") print(f"๐Ÿช Shops deleted: {deleted_shops}") - print(f"๐Ÿ“Š Total items deleted: {deleted_events + deleted_groceries + deleted_shops}") + print(f"๐Ÿ“Š Total items deleted: {deleted_events + deleted_products + deleted_shops}") print("\n๐ŸŽ‰ Cleanup completed successfully!") print("The database is now empty and ready for fresh data.") diff --git a/backend/create_test_data.py b/backend/create_test_data.py index 8f5962d..28ccc3f 100644 --- a/backend/create_test_data.py +++ b/backend/create_test_data.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -Script to create comprehensive test data for the Grocery Tracker application. -This includes shops, groceries, and shopping events with realistic data. +Script to create comprehensive test data for the Product Tracker application. +This includes shops, products, and shopping events with realistic data. """ import requests @@ -20,7 +20,7 @@ SHOPS_DATA = [ {"name": "Safeway", "city": "San Francisco", "address": "2020 Market St"}, {"name": "Trader Joe's", "city": "Berkeley", "address": "1885 University Ave"}, {"name": "Berkeley Bowl", "city": "Berkeley", "address": "2020 Oregon St"}, - {"name": "Rainbow Grocery", "city": "San Francisco", "address": "1745 Folsom St"}, + {"name": "Rainbow Product", "city": "San Francisco", "address": "1745 Folsom St"}, {"name": "Mollie Stone's Market", "city": "Palo Alto", "address": "164 S California Ave"}, {"name": "Costco Wholesale", "city": "San Mateo", "address": "2300 S Norfolk St"}, {"name": "Target", "city": "Mountain View", "address": "1200 El Camino Real"}, @@ -119,13 +119,13 @@ PRICE_RANGES = { def parse_arguments(): """Parse command line arguments.""" - parser = argparse.ArgumentParser(description='Create test data for Grocery Tracker') + parser = argparse.ArgumentParser(description='Create test data for Product Tracker') parser.add_argument('--events', type=int, default=30, help='Number of shopping events to create (default: 30)') parser.add_argument('--days', type=int, default=90, help='Number of days back to generate events (default: 90)') parser.add_argument('--url', type=str, default=BASE_URL, help='API base URL (default: http://localhost:8000)') parser.add_argument('--shops-only', action='store_true', help='Create only shops') - parser.add_argument('--groceries-only', action='store_true', help='Create only groceries') - parser.add_argument('--events-only', action='store_true', help='Create only shopping events (requires existing shops and groceries)') + parser.add_argument('--products-only', action='store_true', help='Create only products') + parser.add_argument('--events-only', action='store_true', help='Create only shopping events (requires existing shops and products)') parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output') parser.add_argument('--dry-run', action='store_true', help='Show what would be created without actually creating it') return parser.parse_args() @@ -171,43 +171,43 @@ def create_shops(base_url: str, verbose: bool = False, dry_run: bool = False) -> print(f" ๐Ÿ“Š Created {len(created_shops)} shops total\n") return created_shops -def create_groceries(base_url: str, verbose: bool = False, dry_run: bool = False) -> List[Dict[str, Any]]: - """Create groceries and return the created grocery objects.""" - print("๐Ÿฅฌ Creating groceries...") - created_groceries = [] +def create_products(base_url: str, verbose: bool = False, dry_run: bool = False) -> List[Dict[str, Any]]: + """Create products and return the created product objects.""" + print("๐Ÿฅฌ Creating products...") + created_products = [] if dry_run: - print(" [DRY RUN] Would create the following groceries:") - for grocery_data in GROCERIES_DATA: - organic_label = "๐ŸŒฑ" if grocery_data['organic'] else "๐ŸŒพ" - print(f" ๐Ÿ“‹ {organic_label} {grocery_data['name']} ({grocery_data['category']})") + print(" [DRY RUN] Would create the following products:") + for product_data in GROCERIES_DATA: + organic_label = "๐ŸŒฑ" if product_data['organic'] else "๐ŸŒพ" + print(f" ๐Ÿ“‹ {organic_label} {product_data['name']} ({product_data['category']})") return [] - for grocery_data in GROCERIES_DATA: + for product_data in GROCERIES_DATA: try: if verbose: - print(f" ๐Ÿ”„ Creating grocery: {grocery_data['name']}...") + print(f" ๐Ÿ”„ Creating product: {product_data['name']}...") - response = requests.post(f"{base_url}/groceries/", json=grocery_data, timeout=10) + response = requests.post(f"{base_url}/products/", json=product_data, timeout=10) if response.status_code == 200: - grocery = response.json() - created_groceries.append(grocery) - organic_label = "๐ŸŒฑ" if grocery['organic'] else "๐ŸŒพ" - print(f" โœ… Created grocery: {organic_label} {grocery['name']} ({grocery['category']})") + product = response.json() + created_products.append(product) + organic_label = "๐ŸŒฑ" if product['organic'] else "๐ŸŒพ" + print(f" โœ… Created product: {organic_label} {product['name']} ({product['category']})") else: - print(f" โŒ Failed to create grocery {grocery_data['name']}: {response.status_code}") + print(f" โŒ Failed to create product {product_data['name']}: {response.status_code}") if verbose: print(f" Response: {response.text}") except requests.exceptions.RequestException as e: - print(f" โŒ Network error creating grocery {grocery_data['name']}: {e}") + print(f" โŒ Network error creating product {product_data['name']}: {e}") except Exception as e: - print(f" โŒ Error creating grocery {grocery_data['name']}: {e}") + print(f" โŒ Error creating product {product_data['name']}: {e}") - print(f" ๐Ÿ“Š Created {len(created_groceries)} groceries total\n") - return created_groceries + print(f" ๐Ÿ“Š Created {len(created_products)} products total\n") + return created_products def generate_random_price(category: str, organic: bool = False) -> float: - """Generate a random price for a grocery item based on category and organic status.""" + """Generate a random price for a product item based on category and organic status.""" min_price, max_price = PRICE_RANGES.get(category, (1.99, 9.99)) # Organic items are typically 20-50% more expensive @@ -220,23 +220,23 @@ def generate_random_price(category: str, organic: bool = False) -> float: return round(price, 2) def get_existing_data(base_url: str) -> tuple[List[Dict], List[Dict]]: - """Get existing shops and groceries from the API.""" + """Get existing shops and products from the API.""" try: shops_response = requests.get(f"{base_url}/shops/", timeout=10) - groceries_response = requests.get(f"{base_url}/groceries/", timeout=10) + products_response = requests.get(f"{base_url}/products/", timeout=10) shops = shops_response.json() if shops_response.status_code == 200 else [] - groceries = groceries_response.json() if groceries_response.status_code == 200 else [] + products = products_response.json() if products_response.status_code == 200 else [] - return shops, groceries + return shops, products except requests.exceptions.RequestException as e: print(f" โŒ Error fetching existing data: {e}") return [], [] -def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: str, +def create_shopping_events(shops: List[Dict], products: List[Dict], base_url: str, num_events: int = 25, days_back: int = 90, verbose: bool = False, dry_run: bool = False) -> List[Dict[str, Any]]: - """Create shopping events with random groceries and realistic data.""" + """Create shopping events with random products and realistic data.""" print(f"๐Ÿ›’ Creating {num_events} shopping events...") created_events = [] @@ -244,8 +244,8 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s print(" โŒ No shops available. Cannot create shopping events.") return [] - if not groceries: - print(" โŒ No groceries available. Cannot create shopping events.") + if not products: + print(" โŒ No products available. Cannot create shopping events.") return [] # Generate events over the specified time period @@ -256,7 +256,7 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s print(f" [DRY RUN] Would create {num_events} shopping events over {days_back} days") print(f" ๐Ÿ“‹ Date range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}") print(f" ๐Ÿ“‹ Available shops: {len(shops)}") - print(f" ๐Ÿ“‹ Available groceries: {len(groceries)}") + print(f" ๐Ÿ“‹ Available products: {len(products)}") return [] for i in range(num_events): @@ -272,32 +272,32 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s minutes=random.randint(0, 59) ) - # Random number of groceries (2-8 items per shopping trip) - num_groceries = random.randint(2, 8) - selected_groceries = random.sample(groceries, min(num_groceries, len(groceries))) + # Random number of products (2-8 items per shopping trip) + num_products = random.randint(2, 8) + selected_products = random.sample(products, min(num_products, len(products))) - # Create grocery items for this event - event_groceries = [] + # Create product items for this event + event_products = [] total_amount = 0.0 - for grocery in selected_groceries: + for product in selected_products: # Random amount based on item type - if grocery['weight_unit'] == 'piece': + if product['weight_unit'] == 'piece': amount = random.randint(1, 4) - elif grocery['weight_unit'] == 'dozen': + elif product['weight_unit'] == 'dozen': amount = 1 - elif grocery['weight_unit'] in ['box', 'head', 'bunch']: + elif product['weight_unit'] in ['box', 'head', 'bunch']: amount = random.randint(1, 2) - elif grocery['weight_unit'] in ['gallon', 'l']: + elif product['weight_unit'] in ['gallon', 'l']: amount = 1 else: amount = round(random.uniform(0.5, 3.0), 2) # Generate price based on category and organic status - price = generate_random_price(grocery['category'], grocery['organic']) + price = generate_random_price(product['category'], product['organic']) - event_groceries.append({ - "grocery_id": grocery['id'], + event_products.append({ + "product_id": product['id'], "amount": amount, "price": price }) @@ -311,11 +311,11 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s notes = None if random.random() < 0.3: note_options = [ - "Weekly grocery shopping", + "Weekly product shopping", "Quick lunch ingredients", "Dinner party prep", "Meal prep for the week", - "Emergency grocery run", + "Emergency product run", "Organic produce haul", "Bulk shopping trip", "Special occasion shopping", @@ -332,14 +332,14 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s "date": event_date.isoformat(), "total_amount": total_amount, "notes": notes, - "groceries": event_groceries + "products": event_products } response = requests.post(f"{base_url}/shopping-events/", json=event_data, timeout=15) if response.status_code == 200: event = response.json() created_events.append(event) - print(f" โœ… Created event #{event['id']}: {shop['name']} - ${total_amount:.2f} ({len(event_groceries)} items)") + print(f" โœ… Created event #{event['id']}: {shop['name']} - ${total_amount:.2f} ({len(event_products)} items)") else: print(f" โŒ Failed to create shopping event: {response.status_code}") if verbose: @@ -352,7 +352,7 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s print(f" ๐Ÿ“Š Created {len(created_events)} shopping events total\n") return created_events -def print_summary(shops: List[Dict], groceries: List[Dict], events: List[Dict]): +def print_summary(shops: List[Dict], products: List[Dict], events: List[Dict]): """Print a summary of the created test data.""" print("๐Ÿ“‹ TEST DATA SUMMARY") print("=" * 50) @@ -361,13 +361,13 @@ def print_summary(shops: List[Dict], groceries: List[Dict], events: List[Dict]): for shop in shops: print(f" โ€ข {shop['name']} ({shop['city']})") - print(f"\n๐Ÿฅฌ Groceries: {len(groceries)}") + print(f"\n๐Ÿฅฌ Products: {len(products)}") categories = {} - for grocery in groceries: - category = grocery['category'] + for product in products: + category = product['category'] if category not in categories: categories[category] = [] - categories[category].append(grocery) + categories[category].append(product) for category, items in categories.items(): organic_count = sum(1 for item in items if item['organic']) @@ -394,7 +394,7 @@ def main(): """Main function to create all test data.""" args = parse_arguments() - print("๐Ÿš€ GROCERY TRACKER TEST DATA GENERATOR") + print("๐Ÿš€ PRODUCT TRACKER TEST DATA GENERATOR") print("=" * 50) if args.dry_run: @@ -415,28 +415,28 @@ def main(): print("โœ… Connected to API server\n") shops = [] - groceries = [] + products = [] events = [] # Create data based on arguments if args.shops_only: shops = create_shops(args.url, args.verbose, args.dry_run) - elif args.groceries_only: - groceries = create_groceries(args.url, args.verbose, args.dry_run) + elif args.products_only: + products = create_products(args.url, args.verbose, args.dry_run) elif args.events_only: # Get existing data for events - shops, groceries = get_existing_data(args.url) - events = create_shopping_events(shops, groceries, args.url, args.events, args.days, args.verbose, args.dry_run) + shops, products = get_existing_data(args.url) + events = create_shopping_events(shops, products, args.url, args.events, args.days, args.verbose, args.dry_run) else: # Create all data shops = create_shops(args.url, args.verbose, args.dry_run) - groceries = create_groceries(args.url, args.verbose, args.dry_run) - if shops and groceries: - events = create_shopping_events(shops, groceries, args.url, args.events, args.days, args.verbose, args.dry_run) + products = create_products(args.url, args.verbose, args.dry_run) + if shops and products: + events = create_shopping_events(shops, products, args.url, args.events, args.days, args.verbose, args.dry_run) # Print summary if not args.dry_run: - print_summary(shops, groceries, events) + print_summary(shops, products, events) if args.dry_run: print("\n๐Ÿ” Dry run completed. Use without --dry-run to actually create the data.") diff --git a/backend/database.py b/backend/database.py index aed8761..56fec59 100644 --- a/backend/database.py +++ b/backend/database.py @@ -11,7 +11,7 @@ DATABASE_URL = os.getenv("DATABASE_URL") if not DATABASE_URL: # Default to SQLite for development if no PostgreSQL URL is provided - DATABASE_URL = "sqlite:///./grocery_tracker.db" + DATABASE_URL = "sqlite:///./product_tracker.db" print("๐Ÿ”„ Using SQLite database for development") else: print(f"๐Ÿ˜ Using PostgreSQL database") diff --git a/backend/env.example b/backend/env.example index a2512a3..184ce16 100644 --- a/backend/env.example +++ b/backend/env.example @@ -1,9 +1,9 @@ # Database Configuration # Option 1: PostgreSQL (for production) -# DATABASE_URL=postgresql://username:password@localhost:5432/grocery_tracker +# DATABASE_URL=postgresql://username:password@localhost:5432/product_tracker # Option 2: SQLite (for development - default if DATABASE_URL is not set) -# DATABASE_URL=sqlite:///./grocery_tracker.db +# DATABASE_URL=sqlite:///./product_tracker.db # Authentication (optional for basic setup) SECRET_KEY=your-secret-key-here diff --git a/backend/main.py b/backend/main.py index 4f10714..1d75b7b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -10,8 +10,8 @@ from database import engine, get_db models.Base.metadata.create_all(bind=engine) app = FastAPI( - title="Grocery Tracker API", - description="API for tracking grocery prices and shopping events", + title="Product Tracker API", + description="API for tracking product prices and shopping events", version="1.0.0" ) @@ -25,22 +25,22 @@ app.add_middleware( ) def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> schemas.ShoppingEventResponse: - """Build a shopping event response with groceries from the association table""" - # Get groceries with their event-specific data - grocery_data = db.execute( + """Build a shopping event response with products from the association table""" + # Get products with their event-specific data + product_data = db.execute( text(""" - SELECT g.id, g.name, g.category, g.organic, g.weight, g.weight_unit, - seg.amount, seg.price - FROM groceries g - JOIN shopping_event_groceries seg ON g.id = seg.grocery_id - WHERE seg.shopping_event_id = :event_id + SELECT p.id, p.name, p.category, p.organic, p.weight, p.weight_unit, + sep.amount, sep.price + FROM products p + JOIN shopping_event_products sep ON p.id = sep.product_id + WHERE sep.shopping_event_id = :event_id """), {"event_id": event.id} ).fetchall() - # Convert to GroceryWithEventData objects - groceries_with_data = [ - schemas.GroceryWithEventData( + # Convert to ProductWithEventData objects + products_with_data = [ + schemas.ProductWithEventData( id=row.id, name=row.name, category=row.category, @@ -50,7 +50,7 @@ def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> s amount=row.amount, price=row.price ) - for row in grocery_data + for row in product_data ] return schemas.ShoppingEventResponse( @@ -61,58 +61,58 @@ def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> s notes=event.notes, created_at=event.created_at, shop=event.shop, - groceries=groceries_with_data + products=products_with_data ) # Root endpoint @app.get("/") def read_root(): - return {"message": "Grocery Tracker API", "version": "1.0.0"} + return {"message": "Product Tracker API", "version": "1.0.0"} -# Grocery endpoints -@app.post("/groceries/", response_model=schemas.Grocery) -def create_grocery(grocery: schemas.GroceryCreate, db: Session = Depends(get_db)): - db_grocery = models.Grocery(**grocery.dict()) - db.add(db_grocery) +# Product endpoints +@app.post("/products/", response_model=schemas.Product) +def create_product(product: schemas.ProductCreate, db: Session = Depends(get_db)): + db_product = models.Product(**product.dict()) + db.add(db_product) db.commit() - db.refresh(db_grocery) - return db_grocery + db.refresh(db_product) + return db_product -@app.get("/groceries/", response_model=List[schemas.Grocery]) -def read_groceries(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): - groceries = db.query(models.Grocery).offset(skip).limit(limit).all() - return groceries +@app.get("/products/", response_model=List[schemas.Product]) +def read_products(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + products = db.query(models.Product).offset(skip).limit(limit).all() + return products -@app.get("/groceries/{grocery_id}", response_model=schemas.Grocery) -def read_grocery(grocery_id: int, db: Session = Depends(get_db)): - grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first() - if grocery is None: - raise HTTPException(status_code=404, detail="Grocery not found") - return grocery +@app.get("/products/{product_id}", response_model=schemas.Product) +def read_product(product_id: int, db: Session = Depends(get_db)): + product = db.query(models.Product).filter(models.Product.id == product_id).first() + if product is None: + raise HTTPException(status_code=404, detail="Product not found") + return product -@app.put("/groceries/{grocery_id}", response_model=schemas.Grocery) -def update_grocery(grocery_id: int, grocery_update: schemas.GroceryUpdate, db: Session = Depends(get_db)): - grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first() - if grocery is None: - raise HTTPException(status_code=404, detail="Grocery not found") +@app.put("/products/{product_id}", response_model=schemas.Product) +def update_product(product_id: int, product_update: schemas.ProductUpdate, db: Session = Depends(get_db)): + product = db.query(models.Product).filter(models.Product.id == product_id).first() + if product is None: + raise HTTPException(status_code=404, detail="Product not found") - update_data = grocery_update.dict(exclude_unset=True) + update_data = product_update.dict(exclude_unset=True) for field, value in update_data.items(): - setattr(grocery, field, value) + setattr(product, field, value) db.commit() - db.refresh(grocery) - return grocery + db.refresh(product) + return product -@app.delete("/groceries/{grocery_id}") -def delete_grocery(grocery_id: int, db: Session = Depends(get_db)): - grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first() - if grocery is None: - raise HTTPException(status_code=404, detail="Grocery not found") +@app.delete("/products/{product_id}") +def delete_product(product_id: int, db: Session = Depends(get_db)): + product = db.query(models.Product).filter(models.Product.id == product_id).first() + if product is None: + raise HTTPException(status_code=404, detail="Product not found") - db.delete(grocery) + db.delete(product) db.commit() - return {"message": "Grocery deleted successfully"} + return {"message": "Product deleted successfully"} # Shop endpoints @app.post("/shops/", response_model=schemas.Shop) @@ -178,19 +178,19 @@ def create_shopping_event(event: schemas.ShoppingEventCreate, db: Session = Depe db.commit() db.refresh(db_event) - # Add groceries to the event - for grocery_item in event.groceries: - grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_item.grocery_id).first() - if grocery is None: - raise HTTPException(status_code=404, detail=f"Grocery with id {grocery_item.grocery_id} not found") + # Add products to the event + for product_item in event.products: + product = db.query(models.Product).filter(models.Product.id == product_item.product_id).first() + if product is None: + raise HTTPException(status_code=404, detail=f"Product with id {product_item.product_id} not found") # Insert into association table db.execute( - models.shopping_event_groceries.insert().values( + models.shopping_event_products.insert().values( shopping_event_id=db_event.id, - grocery_id=grocery_item.grocery_id, - amount=grocery_item.amount, - price=grocery_item.price + product_id=product_item.product_id, + amount=product_item.amount, + price=product_item.price ) ) @@ -228,26 +228,26 @@ def update_shopping_event(event_id: int, event_update: schemas.ShoppingEventCrea event.total_amount = event_update.total_amount event.notes = event_update.notes - # Remove existing grocery associations + # Remove existing product associations db.execute( - models.shopping_event_groceries.delete().where( - models.shopping_event_groceries.c.shopping_event_id == event_id + models.shopping_event_products.delete().where( + models.shopping_event_products.c.shopping_event_id == event_id ) ) - # Add new grocery associations - for grocery_item in event_update.groceries: - grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_item.grocery_id).first() - if grocery is None: - raise HTTPException(status_code=404, detail=f"Grocery with id {grocery_item.grocery_id} not found") + # Add new product associations + for product_item in event_update.products: + product = db.query(models.Product).filter(models.Product.id == product_item.product_id).first() + if product is None: + raise HTTPException(status_code=404, detail=f"Product with id {product_item.product_id} not found") # Insert into association table db.execute( - models.shopping_event_groceries.insert().values( + models.shopping_event_products.insert().values( shopping_event_id=event_id, - grocery_id=grocery_item.grocery_id, - amount=grocery_item.amount, - price=grocery_item.price + product_id=product_item.product_id, + amount=product_item.amount, + price=product_item.price ) ) @@ -261,10 +261,10 @@ def delete_shopping_event(event_id: int, db: Session = Depends(get_db)): if event is None: raise HTTPException(status_code=404, detail="Shopping event not found") - # Delete grocery associations first + # Delete product associations first db.execute( - models.shopping_event_groceries.delete().where( - models.shopping_event_groceries.c.shopping_event_id == event_id + models.shopping_event_products.delete().where( + models.shopping_event_products.c.shopping_event_id == event_id ) ) diff --git a/backend/models.py b/backend/models.py index f2effba..ab74940 100644 --- a/backend/models.py +++ b/backend/models.py @@ -6,19 +6,19 @@ from datetime import datetime Base = declarative_base() -# Association table for many-to-many relationship between shopping events and groceries -shopping_event_groceries = Table( - 'shopping_event_groceries', +# Association table for many-to-many relationship between shopping events and products +shopping_event_products = Table( + 'shopping_event_products', Base.metadata, Column('id', Integer, primary_key=True, autoincrement=True), # Artificial primary key Column('shopping_event_id', Integer, ForeignKey('shopping_events.id'), nullable=False), - Column('grocery_id', Integer, ForeignKey('groceries.id'), nullable=False), - Column('amount', Float, nullable=False), # Amount of this grocery bought in this event - Column('price', Float, nullable=False) # Price of this grocery at the time of this shopping event + Column('product_id', Integer, ForeignKey('products.id'), nullable=False), + Column('amount', Float, nullable=False), # Amount of this product bought in this event + Column('price', Float, nullable=False) # Price of this product at the time of this shopping event ) -class Grocery(Base): - __tablename__ = "groceries" +class Product(Base): + __tablename__ = "products" id = Column(Integer, primary_key=True, index=True) name = Column(String, nullable=False, index=True) @@ -30,7 +30,7 @@ class Grocery(Base): updated_at = Column(DateTime(timezone=True), onupdate=func.now()) # Relationships - shopping_events = relationship("ShoppingEvent", secondary=shopping_event_groceries, back_populates="groceries") + shopping_events = relationship("ShoppingEvent", secondary=shopping_event_products, back_populates="products") class Shop(Base): __tablename__ = "shops" @@ -58,4 +58,4 @@ class ShoppingEvent(Base): # Relationships shop = relationship("Shop", back_populates="shopping_events") - groceries = relationship("Grocery", secondary=shopping_event_groceries, back_populates="shopping_events") \ No newline at end of file + products = relationship("Product", secondary=shopping_event_products, back_populates="shopping_events") \ No newline at end of file diff --git a/backend/run_dev.py b/backend/run_dev.py index 85235b7..74acdb8 100644 --- a/backend/run_dev.py +++ b/backend/run_dev.py @@ -22,7 +22,7 @@ def main(): backend_dir = Path(__file__).parent os.chdir(backend_dir) - print("๐Ÿƒ Starting Grocery Tracker Backend Development Server") + print("๐Ÿƒ Starting Product Tracker Backend Development Server") print("=" * 50) # Check if virtual environment exists diff --git a/backend/schemas.py b/backend/schemas.py index 2e55a62..b6c3cff 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -3,24 +3,24 @@ from typing import Optional, List from datetime import datetime # Base schemas -class GroceryBase(BaseModel): +class ProductBase(BaseModel): name: str category: str organic: bool = False weight: Optional[float] = None weight_unit: str = "g" -class GroceryCreate(GroceryBase): +class ProductCreate(ProductBase): pass -class GroceryUpdate(BaseModel): +class ProductUpdate(BaseModel): name: Optional[str] = None category: Optional[str] = None organic: Optional[bool] = None weight: Optional[float] = None weight_unit: Optional[str] = None -class Grocery(GroceryBase): +class Product(ProductBase): id: int created_at: datetime updated_at: Optional[datetime] = None @@ -51,12 +51,12 @@ class Shop(ShopBase): from_attributes = True # Shopping Event schemas -class GroceryInEvent(BaseModel): - grocery_id: int +class ProductInEvent(BaseModel): + product_id: int amount: float = Field(..., gt=0) price: float = Field(..., ge=0) # Price at the time of this shopping event (allow free items) -class GroceryWithEventData(BaseModel): +class ProductWithEventData(BaseModel): id: int name: str category: str @@ -76,21 +76,21 @@ class ShoppingEventBase(BaseModel): notes: Optional[str] = None class ShoppingEventCreate(ShoppingEventBase): - groceries: List[GroceryInEvent] = [] + products: List[ProductInEvent] = [] class ShoppingEventUpdate(BaseModel): shop_id: Optional[int] = None date: Optional[datetime] = None total_amount: Optional[float] = Field(None, ge=0) notes: Optional[str] = None - groceries: Optional[List[GroceryInEvent]] = None + products: Optional[List[ProductInEvent]] = None class ShoppingEventResponse(ShoppingEventBase): id: int created_at: datetime updated_at: Optional[datetime] = None shop: Shop - groceries: List[GroceryWithEventData] = [] + products: List[ProductWithEventData] = [] class Config: from_attributes = True diff --git a/database_schema dev.drawio b/database_schema dev.drawio new file mode 100644 index 0000000..a411978 --- /dev/null +++ b/database_schema dev.drawio @@ -0,0 +1,603 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/database_schema.drawio b/database_schema.drawio index 44dda67..7b60760 100644 --- a/database_schema.drawio +++ b/database_schema.drawio @@ -1,5 +1,5 @@ - + @@ -22,7 +22,7 @@ - + @@ -32,7 +32,7 @@ - + @@ -50,10 +50,10 @@ - + - + @@ -335,7 +335,7 @@ - + @@ -372,7 +372,7 @@ - + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4b6ec72..fab20cc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "grocery-tracker-frontend", + "name": "product-tracker-frontend", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "grocery-tracker-frontend", + "name": "product-tracker-frontend", "version": "0.1.0", "dependencies": { "@types/node": "^20.10.5", diff --git a/frontend/package.json b/frontend/package.json index d41ca0f..161fd71 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "grocery-tracker-frontend", + "name": "product-tracker-frontend", "version": "0.1.0", "private": true, "dependencies": { diff --git a/frontend/public/index.html b/frontend/public/index.html index b919614..4590176 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -7,9 +7,9 @@ - Grocery Tracker + Product Tracker diff --git a/frontend/setup.sh b/frontend/setup.sh index 7d15f46..eb466f0 100755 --- a/frontend/setup.sh +++ b/frontend/setup.sh @@ -1,6 +1,6 @@ #!/bin/bash -echo "๐ŸŽฏ Setting up Grocery Tracker Frontend" +echo "๐ŸŽฏ Setting up Product Tracker Frontend" echo "======================================" # Check if Node.js is installed diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a6c33dd..65d9b8c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 ( + + ); +} function App() { return ( -
- {/* Navigation */} - - - {/* Main Content */} -
+
+ +
} /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/AddGroceryModal.tsx b/frontend/src/components/AddProductModal.tsx similarity index 84% rename from frontend/src/components/AddGroceryModal.tsx rename to frontend/src/components/AddProductModal.tsx index b8e4f2f..7a3d279 100644 --- a/frontend/src/components/AddGroceryModal.tsx +++ b/frontend/src/components/AddProductModal.tsx @@ -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 = ({ isOpen, onClose, onGroceryAdded, editGrocery }) => { - const [formData, setFormData] = useState({ +const AddProductModal: React.FC = ({ isOpen, onClose, onProductAdded, editProduct }) => { + const [formData, setFormData] = useState({ name: '', category: '', organic: false, @@ -37,16 +37,16 @@ const AddGroceryModal: React.FC = ({ 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 = ({ isOpen, onClose, onGr }); } setError(''); - }, [editGrocery, isOpen]); + }, [editProduct, isOpen]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -69,17 +69,17 @@ const AddGroceryModal: React.FC = ({ 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 = ({ 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 = ({ isOpen, onClose, onGr

- {editGrocery ? 'Edit Grocery' : 'Add New Grocery'} + {editProduct ? 'Edit Product' : 'Add New Product'}

@@ -248,4 +248,4 @@ const AddGroceryModal: React.FC = ({ isOpen, onClose, onGr ); }; -export default AddGroceryModal; \ No newline at end of file +export default AddProductModal; \ No newline at end of file diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index bd57bd4..7420c16 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -32,7 +32,7 @@ const Dashboard: React.FC = () => {

Dashboard

-

Welcome to your grocery tracker!

+

Welcome to your product tracker!

@@ -102,7 +102,7 @@ const Dashboard: React.FC = () => {
@@ -181,9 +181,9 @@ const Dashboard: React.FC = () => {

{new Date(event.date).toLocaleDateString()}

- {event.groceries.length > 0 && ( + {event.products.length > 0 && (

- {event.groceries.length} item{event.groceries.length !== 1 ? 's' : ''} + {event.products.length} item{event.products.length !== 1 ? 's' : ''}

)}
diff --git a/frontend/src/components/GroceryList.tsx b/frontend/src/components/ProductList.tsx similarity index 69% rename from frontend/src/components/GroceryList.tsx rename to frontend/src/components/ProductList.tsx index 97e3eec..7e83570 100644 --- a/frontend/src/components/GroceryList.tsx +++ b/frontend/src/components/ProductList.tsx @@ -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([]); + const [products, setProducts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [isModalOpen, setIsModalOpen] = useState(false); - const [editingGrocery, setEditingGrocery] = useState(null); - const [deletingGrocery, setDeletingGrocery] = useState(null); + const [editingProduct, setEditingProduct] = useState(null); + const [deletingProduct, setDeletingProduct] = useState(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 (
-

Groceries

+

Products

@@ -107,13 +107,13 @@ const GroceryList: React.FC = () => { )}
- {groceries.length === 0 ? ( + {products.length === 0 ? (
-

No groceries

-

Get started by adding your first grocery item.

+

No products

+

Get started by adding your first product item.

) : ( @@ -137,39 +137,39 @@ const GroceryList: React.FC = () => { - {groceries.map((grocery) => ( - + {products.map((product) => ( +
- {grocery.name} {grocery.organic ? '๐ŸŒฑ' : ''} + {product.name} {product.organic ? '๐ŸŒฑ' : ''}
- {grocery.category} + {product.category} - {grocery.weight ? `${grocery.weight}${grocery.weight_unit}` : '-'} + {product.weight ? `${product.weight}${product.weight_unit}` : '-'} - {grocery.organic ? 'Organic' : 'Conventional'} + {product.organic ? 'Organic' : 'Conventional'}