From 6118415f05c9dc1b698885852ba777f7318f65aa Mon Sep 17 00:00:00 2001 From: lasse Date: Mon, 26 May 2025 21:13:49 +0200 Subject: [PATCH] cleanup --- backend/TEST_DATA_README.md | 222 --------- backend/cleanup_test_data.py | 200 -------- backend/create_test_data.py | 459 ------------------ database_schema dev.drawio | 2 +- database_schema.drawio | 114 ++++- frontend/src/components/ShoppingEventForm.tsx | 80 +-- 6 files changed, 132 insertions(+), 945 deletions(-) delete mode 100644 backend/TEST_DATA_README.md delete mode 100644 backend/cleanup_test_data.py delete mode 100644 backend/create_test_data.py diff --git a/backend/TEST_DATA_README.md b/backend/TEST_DATA_README.md deleted file mode 100644 index 614e6a3..0000000 --- a/backend/TEST_DATA_README.md +++ /dev/null @@ -1,222 +0,0 @@ -# Test Data Scripts Documentation - -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, products, and shopping events. - -#### Basic Usage - -```bash -# Create all test data (default: 30 events over 90 days) -python create_test_data.py - -# Create with custom parameters -python create_test_data.py --events 50 --days 120 - -# Verbose output -python create_test_data.py --verbose - -# Dry run (see what would be created without creating it) -python create_test_data.py --dry-run -``` - -#### Command Line Options - -| Option | Description | Default | -|--------|-------------|---------| -| `--events N` | Number of shopping events to create | 30 | -| `--days N` | Number of days back to generate events | 90 | -| `--url URL` | API base URL | http://localhost:8000 | -| `--shops-only` | Create only shops | 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 | - -#### Examples - -```bash -# Create only shops -python create_test_data.py --shops-only - -# Create only products -python create_test_data.py --products-only - -# 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 -python create_test_data.py --events 60 --days 180 --verbose - -# Preview what would be created without actually creating it -python create_test_data.py --dry-run - -# Use a different API URL -python create_test_data.py --url http://localhost:3000 -``` - -### 2. `cleanup_test_data.py` - Data Cleanup Script - -Safely removes all test data with confirmation prompts. - -```bash -python cleanup_test_data.py -``` - -## Data Structure - -### Shops (10 total) -- **Whole Foods Market** (San Francisco) -- **Safeway** (San Francisco) -- **Trader Joe's** (Berkeley) -- **Berkeley Bowl** (Berkeley) -- **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) - -### Products (50+ items across 8 categories) - -| Category | Items | Organic Options | -|----------|-------|-----------------| -| **Fruits** | 10 items | 5 organic | -| **Vegetables** | 10 items | 5 organic | -| **Dairy** | 7 items | 4 organic | -| **Meat & Seafood** | 6 items | 3 organic | -| **Pantry** | 10 items | 5 organic | -| **Beverages** | 6 items | 3 organic | -| **Frozen** | 5 items | 2 organic | -| **Snacks** | 5 items | 3 organic | - -### Shopping Events -- **Realistic dates**: Distributed over specified time period (default: 90 days) -- **Smart quantities**: Appropriate amounts based on item type -- **Category-based pricing**: Realistic price ranges per category -- **Organic premiums**: 20-50% higher prices for organic items -- **Random notes**: 30% of events include descriptive notes -- **Varied trip sizes**: 2-8 items per shopping trip - -## Features - -### Smart Data Generation -- **Realistic pricing**: Category-based price ranges with organic premiums -- **Appropriate quantities**: Items sold by piece, weight, or volume as appropriate -- **Temporal distribution**: Events spread realistically over time -- **Shopping patterns**: Varied trip sizes and frequencies - -### Error Handling -- **Graceful failures**: Script continues even if some items fail -- **Network timeouts**: Reasonable timeout values for API calls -- **Progress tracking**: Clear feedback on creation progress -- **Connection testing**: Verifies API availability before starting - -### Flexible Options -- **Partial creation**: Create only specific data types -- **Custom parameters**: Adjust event count and date range -- **Dry run mode**: Preview without creating data -- **Verbose output**: Detailed progress information -- **Custom API URLs**: Support for different backend configurations - -## Troubleshooting - -### Common Issues - -1. **Connection Error** - ``` - โŒ Cannot connect to the API server at http://localhost:8000 - ``` - **Solution**: Make sure the backend server is running: - ```bash - cd backend - uvicorn main:app --reload - ``` - -2. **Module Not Found Error** - ``` - ModuleNotFoundError: No module named 'requests' - ``` - **Solution**: Install dependencies: - ```bash - pip install -r requirements.txt - ``` - -3. **Database Connection Error** - **Solution**: Ensure PostgreSQL is running and database exists: - ```bash - # Check if PostgreSQL is running - brew services list | grep postgresql - - # Start PostgreSQL if needed - brew services start postgresql - ``` - -4. **Partial Data Creation** - If some items fail to create, the script will continue and report the final count. - Use `--verbose` to see detailed error messages. - -### Performance Tips - -- **Large datasets**: For creating many events (100+), consider running in smaller batches -- **Network issues**: Use `--verbose` to identify specific failures -- **Database performance**: Ensure your database has adequate resources for bulk operations - -## Data Cleanup - -To remove all test data: - -```bash -python cleanup_test_data.py -``` - -This script will: -1. Show what data exists -2. Ask for confirmation before deletion -3. Handle foreign key constraints properly -4. Provide progress feedback - -## Integration with Application - -After running the test data scripts: - -1. **Frontend**: Refresh the application to see new data -2. **API**: All endpoints will return the test data -3. **Database**: Data is persisted and will survive server restarts - -The test data is designed to showcase all application features: -- Multiple shops and locations -- Diverse product categories -- Realistic shopping patterns -- Price variations and organic options -- Historical data for analytics - -## Advanced Usage - -### Custom Data Sets - -You can modify the data arrays in `create_test_data.py` to create custom test scenarios: - -- Add more shops for specific regions -- Include specialty product categories -- Adjust price ranges for different markets -- Create seasonal shopping patterns - -### Automated Testing - -The scripts can be integrated into automated testing workflows: - -```bash -# Setup test environment -python create_test_data.py --events 10 --days 30 - -# Run tests -pytest - -# Cleanup -python cleanup_test_data.py --force # (if you add a --force option) -``` \ No newline at end of file diff --git a/backend/cleanup_test_data.py b/backend/cleanup_test_data.py deleted file mode 100644 index 90004b2..0000000 --- a/backend/cleanup_test_data.py +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env python3 -""" -Script to clean up all test data from the Product Tracker application. -This will delete all shopping events, products, and shops. -""" - -import requests -import json -from typing import List, Dict, Any - -BASE_URL = "http://localhost:8000" - -def delete_all_shopping_events() -> int: - """Delete all shopping events and return the count of deleted events.""" - print("๐Ÿ›’ Deleting all shopping events...") - - try: - # Get all shopping events - response = requests.get(f"{BASE_URL}/shopping-events/") - if response.status_code != 200: - print(f" โŒ Failed to fetch shopping events: {response.status_code}") - return 0 - - events = response.json() - deleted_count = 0 - - for event in events: - try: - delete_response = requests.delete(f"{BASE_URL}/shopping-events/{event['id']}") - if delete_response.status_code == 200: - deleted_count += 1 - print(f" โœ… Deleted event #{event['id']} from {event['shop']['name']}") - else: - print(f" โŒ Failed to delete event #{event['id']}: {delete_response.status_code}") - except Exception as e: - print(f" โŒ Error deleting event #{event['id']}: {e}") - - print(f" ๐Ÿ“Š Deleted {deleted_count} shopping events total\n") - return deleted_count - - except Exception as e: - print(f" โŒ Error fetching shopping events: {e}") - return 0 - -def delete_all_products() -> int: - """Delete all products and return the count of deleted products.""" - print("๐Ÿฅฌ Deleting all products...") - - try: - # Get all products - response = requests.get(f"{BASE_URL}/products/") - if response.status_code != 200: - print(f" โŒ Failed to fetch products: {response.status_code}") - return 0 - - products = response.json() - deleted_count = 0 - - for product in products: - try: - delete_response = requests.delete(f"{BASE_URL}/products/{product['id']}") - if delete_response.status_code == 200: - deleted_count += 1 - organic_label = "๐ŸŒฑ" if product['organic'] else "๐ŸŒพ" - print(f" โœ… Deleted product: {organic_label} {product['name']}") - else: - print(f" โŒ Failed to delete product {product['name']}: {delete_response.status_code}") - except Exception as e: - print(f" โŒ Error deleting product {product['name']}: {e}") - - print(f" ๐Ÿ“Š Deleted {deleted_count} products total\n") - return deleted_count - - except Exception as e: - print(f" โŒ Error fetching products: {e}") - return 0 - -def delete_all_shops() -> int: - """Delete all shops and return the count of deleted shops.""" - print("๐Ÿช Deleting all shops...") - - try: - # Get all shops - response = requests.get(f"{BASE_URL}/shops/") - if response.status_code != 200: - print(f" โŒ Failed to fetch shops: {response.status_code}") - return 0 - - shops = response.json() - deleted_count = 0 - - for shop in shops: - try: - delete_response = requests.delete(f"{BASE_URL}/shops/{shop['id']}") - if delete_response.status_code == 200: - deleted_count += 1 - print(f" โœ… Deleted shop: {shop['name']} ({shop['city']})") - else: - print(f" โŒ Failed to delete shop {shop['name']}: {delete_response.status_code}") - except Exception as e: - print(f" โŒ Error deleting shop {shop['name']}: {e}") - - print(f" ๐Ÿ“Š Deleted {deleted_count} shops total\n") - return deleted_count - - except Exception as e: - print(f" โŒ Error fetching shops: {e}") - return 0 - -def get_current_data_summary(): - """Get a summary of current data in the database.""" - print("๐Ÿ“Š CURRENT DATA SUMMARY") - print("=" * 30) - - try: - # Get counts - shops_response = requests.get(f"{BASE_URL}/shops/") - 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 - 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"๐Ÿฅฌ Products: {products_count}") - print(f"๐Ÿ›’ Shopping Events: {events_count}") - - if events_count > 0 and events_response.status_code == 200: - events = events_response.json() - total_spent = sum(event.get('total_amount', 0) for event in events) - print(f"๐Ÿ’ฐ Total spent: ${total_spent:.2f}") - - print() - return shops_count, products_count, events_count - - except Exception as e: - print(f"โŒ Error getting data summary: {e}") - return 0, 0, 0 - -def main(): - """Main function to clean up all test data.""" - print("๐Ÿงน PRODUCT TRACKER DATA CLEANUP") - print("=" * 40) - 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: - # Test connection - response = requests.get(f"{BASE_URL}/") - if response.status_code != 200: - print("โŒ Cannot connect to the API server. Make sure it's running!") - return - - print("โœ… Connected to API server\n") - - # Show current data - shops_count, products_count, events_count = get_current_data_summary() - - if shops_count == 0 and products_count == 0 and events_count == 0: - print("โœ… Database is already empty. Nothing to clean up!") - return - - # Confirm deletion - print("โš ๏ธ WARNING: This will permanently delete all data!") - confirmation = input("Type 'DELETE' to confirm: ") - - if confirmation != 'DELETE': - print("โŒ Cleanup cancelled.") - return - - print("\n๐Ÿงน Starting cleanup process...\n") - - # Delete in order: events -> products -> shops - # (due to foreign key constraints) - deleted_events = delete_all_shopping_events() - 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"๐Ÿฅฌ Products deleted: {deleted_products}") - print(f"๐Ÿช Shops deleted: {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.") - - except requests.exceptions.ConnectionError: - print("โŒ Could not connect to the API server.") - print(" Make sure the backend server is running on http://localhost:8000") - except KeyboardInterrupt: - print("\nโŒ Cleanup cancelled by user.") - except Exception as e: - print(f"โŒ Unexpected error: {e}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/backend/create_test_data.py b/backend/create_test_data.py deleted file mode 100644 index 28ccc3f..0000000 --- a/backend/create_test_data.py +++ /dev/null @@ -1,459 +0,0 @@ -#!/usr/bin/env python3 -""" -Script to create comprehensive test data for the Product Tracker application. -This includes shops, products, and shopping events with realistic data. -""" - -import requests -import json -import random -import argparse -import sys -from datetime import datetime, timedelta -from typing import List, Dict, Any - -BASE_URL = "http://localhost:8000" - -# Test data definitions -SHOPS_DATA = [ - {"name": "Whole Foods Market", "city": "San Francisco", "address": "1765 California St"}, - {"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 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"}, - {"name": "Sprouts Farmers Market", "city": "Sunnyvale", "address": "1077 E El Camino Real"}, - {"name": "Lucky Supermarket", "city": "San Jose", "address": "1717 Tully Rd"}, -] - -GROCERIES_DATA = [ - # Fruits - {"name": "Organic Bananas", "category": "Fruits", "organic": True, "weight": 1.0, "weight_unit": "lb"}, - {"name": "Gala Apples", "category": "Fruits", "organic": False, "weight": 2.0, "weight_unit": "lb"}, - {"name": "Organic Strawberries", "category": "Fruits", "organic": True, "weight": 1.0, "weight_unit": "lb"}, - {"name": "Avocados", "category": "Fruits", "organic": False, "weight": None, "weight_unit": "piece"}, - {"name": "Organic Blueberries", "category": "Fruits", "organic": True, "weight": 0.5, "weight_unit": "lb"}, - {"name": "Lemons", "category": "Fruits", "organic": False, "weight": None, "weight_unit": "piece"}, - {"name": "Organic Oranges", "category": "Fruits", "organic": True, "weight": 3.0, "weight_unit": "lb"}, - {"name": "Grapes", "category": "Fruits", "organic": False, "weight": 2.0, "weight_unit": "lb"}, - {"name": "Organic Pears", "category": "Fruits", "organic": True, "weight": 2.0, "weight_unit": "lb"}, - {"name": "Pineapple", "category": "Fruits", "organic": False, "weight": None, "weight_unit": "piece"}, - - # Vegetables - {"name": "Organic Spinach", "category": "Vegetables", "organic": True, "weight": 5.0, "weight_unit": "oz"}, - {"name": "Carrots", "category": "Vegetables", "organic": False, "weight": 2.0, "weight_unit": "lb"}, - {"name": "Organic Broccoli", "category": "Vegetables", "organic": True, "weight": None, "weight_unit": "piece"}, - {"name": "Red Bell Peppers", "category": "Vegetables", "organic": False, "weight": None, "weight_unit": "piece"}, - {"name": "Organic Kale", "category": "Vegetables", "organic": True, "weight": 1.0, "weight_unit": "bunch"}, - {"name": "Tomatoes", "category": "Vegetables", "organic": False, "weight": 2.0, "weight_unit": "lb"}, - {"name": "Organic Sweet Potatoes", "category": "Vegetables", "organic": True, "weight": 3.0, "weight_unit": "lb"}, - {"name": "Cucumbers", "category": "Vegetables", "organic": False, "weight": None, "weight_unit": "piece"}, - {"name": "Organic Lettuce", "category": "Vegetables", "organic": True, "weight": None, "weight_unit": "head"}, - {"name": "Onions", "category": "Vegetables", "organic": False, "weight": 3.0, "weight_unit": "lb"}, - - # Dairy - {"name": "Organic Whole Milk", "category": "Dairy", "organic": True, "weight": 1.0, "weight_unit": "gallon"}, - {"name": "Greek Yogurt", "category": "Dairy", "organic": False, "weight": 32.0, "weight_unit": "oz"}, - {"name": "Organic Eggs", "category": "Dairy", "organic": True, "weight": None, "weight_unit": "dozen"}, - {"name": "Cheddar Cheese", "category": "Dairy", "organic": False, "weight": 8.0, "weight_unit": "oz"}, - {"name": "Organic Butter", "category": "Dairy", "organic": True, "weight": 1.0, "weight_unit": "lb"}, - {"name": "Cream Cheese", "category": "Dairy", "organic": False, "weight": 8.0, "weight_unit": "oz"}, - {"name": "Organic Yogurt", "category": "Dairy", "organic": True, "weight": 6.0, "weight_unit": "oz"}, - - # Meat & Seafood - {"name": "Organic Chicken Breast", "category": "Meat & Seafood", "organic": True, "weight": 2.0, "weight_unit": "lb"}, - {"name": "Ground Beef", "category": "Meat & Seafood", "organic": False, "weight": 1.0, "weight_unit": "lb"}, - {"name": "Wild Salmon Fillet", "category": "Meat & Seafood", "organic": False, "weight": 1.5, "weight_unit": "lb"}, - {"name": "Organic Ground Turkey", "category": "Meat & Seafood", "organic": True, "weight": 1.0, "weight_unit": "lb"}, - {"name": "Shrimp", "category": "Meat & Seafood", "organic": False, "weight": 1.0, "weight_unit": "lb"}, - {"name": "Organic Chicken Thighs", "category": "Meat & Seafood", "organic": True, "weight": 2.0, "weight_unit": "lb"}, - - # Pantry - {"name": "Organic Brown Rice", "category": "Pantry", "organic": True, "weight": 2.0, "weight_unit": "lb"}, - {"name": "Whole Wheat Bread", "category": "Pantry", "organic": False, "weight": 24.0, "weight_unit": "oz"}, - {"name": "Organic Quinoa", "category": "Pantry", "organic": True, "weight": 1.0, "weight_unit": "lb"}, - {"name": "Olive Oil", "category": "Pantry", "organic": False, "weight": 500.0, "weight_unit": "ml"}, - {"name": "Organic Pasta", "category": "Pantry", "organic": True, "weight": 1.0, "weight_unit": "lb"}, - {"name": "Black Beans", "category": "Pantry", "organic": False, "weight": 15.0, "weight_unit": "oz"}, - {"name": "Organic Oats", "category": "Pantry", "organic": True, "weight": 18.0, "weight_unit": "oz"}, - {"name": "Peanut Butter", "category": "Pantry", "organic": False, "weight": 18.0, "weight_unit": "oz"}, - {"name": "Organic Honey", "category": "Pantry", "organic": True, "weight": 12.0, "weight_unit": "oz"}, - {"name": "Canned Tomatoes", "category": "Pantry", "organic": False, "weight": 14.5, "weight_unit": "oz"}, - - # Beverages - {"name": "Organic Orange Juice", "category": "Beverages", "organic": True, "weight": 64.0, "weight_unit": "oz"}, - {"name": "Sparkling Water", "category": "Beverages", "organic": False, "weight": 1.0, "weight_unit": "l"}, - {"name": "Organic Green Tea", "category": "Beverages", "organic": True, "weight": None, "weight_unit": "box"}, - {"name": "Coffee Beans", "category": "Beverages", "organic": False, "weight": 12.0, "weight_unit": "oz"}, - {"name": "Almond Milk", "category": "Beverages", "organic": False, "weight": 32.0, "weight_unit": "oz"}, - {"name": "Organic Apple Juice", "category": "Beverages", "organic": True, "weight": 64.0, "weight_unit": "oz"}, - - # Frozen - {"name": "Organic Frozen Berries", "category": "Frozen", "organic": True, "weight": 10.0, "weight_unit": "oz"}, - {"name": "Frozen Pizza", "category": "Frozen", "organic": False, "weight": 12.0, "weight_unit": "oz"}, - {"name": "Organic Frozen Vegetables", "category": "Frozen", "organic": True, "weight": 16.0, "weight_unit": "oz"}, - {"name": "Ice Cream", "category": "Frozen", "organic": False, "weight": 48.0, "weight_unit": "oz"}, - {"name": "Frozen Fish Fillets", "category": "Frozen", "organic": False, "weight": 1.0, "weight_unit": "lb"}, - - # Snacks - {"name": "Organic Granola Bars", "category": "Snacks", "organic": True, "weight": 8.0, "weight_unit": "oz"}, - {"name": "Potato Chips", "category": "Snacks", "organic": False, "weight": 5.0, "weight_unit": "oz"}, - {"name": "Organic Nuts", "category": "Snacks", "organic": True, "weight": 6.0, "weight_unit": "oz"}, - {"name": "Crackers", "category": "Snacks", "organic": False, "weight": 7.0, "weight_unit": "oz"}, - {"name": "Organic Popcorn", "category": "Snacks", "organic": True, "weight": 3.0, "weight_unit": "oz"}, -] - -# Price ranges for different categories (min, max) -PRICE_RANGES = { - "Fruits": (1.99, 8.99), - "Vegetables": (0.99, 6.99), - "Dairy": (2.49, 12.99), - "Meat & Seafood": (4.99, 24.99), - "Pantry": (1.99, 15.99), - "Beverages": (1.99, 8.99), - "Frozen": (2.99, 9.99), - "Snacks": (1.49, 7.99), -} - -def parse_arguments(): - """Parse command line arguments.""" - 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('--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() - -def check_api_connection(base_url: str) -> bool: - """Check if the API server is accessible.""" - try: - response = requests.get(f"{base_url}/", timeout=5) - return response.status_code == 200 - except requests.exceptions.RequestException: - return False - -def create_shops(base_url: str, verbose: bool = False, dry_run: bool = False) -> List[Dict[str, Any]]: - """Create shops and return the created shop objects.""" - print("๐Ÿช Creating shops...") - created_shops = [] - - if dry_run: - print(" [DRY RUN] Would create the following shops:") - for shop_data in SHOPS_DATA: - print(f" ๐Ÿ“‹ {shop_data['name']} in {shop_data['city']}") - return [] - - for shop_data in SHOPS_DATA: - try: - if verbose: - print(f" ๐Ÿ”„ Creating shop: {shop_data['name']}...") - - response = requests.post(f"{base_url}/shops/", json=shop_data, timeout=10) - if response.status_code == 200: - shop = response.json() - created_shops.append(shop) - print(f" โœ… Created shop: {shop['name']} in {shop['city']}") - else: - print(f" โŒ Failed to create shop {shop_data['name']}: {response.status_code}") - if verbose: - print(f" Response: {response.text}") - except requests.exceptions.RequestException as e: - print(f" โŒ Network error creating shop {shop_data['name']}: {e}") - except Exception as e: - print(f" โŒ Error creating shop {shop_data['name']}: {e}") - - print(f" ๐Ÿ“Š Created {len(created_shops)} shops total\n") - return created_shops - -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 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 product_data in GROCERIES_DATA: - try: - if verbose: - print(f" ๐Ÿ”„ Creating product: {product_data['name']}...") - - response = requests.post(f"{base_url}/products/", json=product_data, timeout=10) - if response.status_code == 200: - 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 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 product {product_data['name']}: {e}") - except Exception as e: - print(f" โŒ Error creating product {product_data['name']}: {e}") - - 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 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 - if organic: - min_price *= 1.2 - max_price *= 1.5 - - # Generate random price and round to nearest cent - price = random.uniform(min_price, max_price) - return round(price, 2) - -def get_existing_data(base_url: str) -> tuple[List[Dict], List[Dict]]: - """Get existing shops and products from the API.""" - try: - shops_response = requests.get(f"{base_url}/shops/", timeout=10) - products_response = requests.get(f"{base_url}/products/", timeout=10) - - shops = shops_response.json() if shops_response.status_code == 200 else [] - products = products_response.json() if products_response.status_code == 200 else [] - - return shops, products - except requests.exceptions.RequestException as e: - print(f" โŒ Error fetching existing data: {e}") - return [], [] - -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 products and realistic data.""" - print(f"๐Ÿ›’ Creating {num_events} shopping events...") - created_events = [] - - if not shops: - print(" โŒ No shops available. Cannot create shopping events.") - return [] - - if not products: - print(" โŒ No products available. Cannot create shopping events.") - return [] - - # Generate events over the specified time period - end_date = datetime.now() - start_date = end_date - timedelta(days=days_back) - - if dry_run: - 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 products: {len(products)}") - return [] - - for i in range(num_events): - try: - if verbose: - print(f" ๐Ÿ”„ Creating shopping event {i+1}/{num_events}...") - - # Random shop and date - shop = random.choice(shops) - event_date = start_date + timedelta( - days=random.randint(0, days_back), - hours=random.randint(8, 20), - minutes=random.randint(0, 59) - ) - - # 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 product items for this event - event_products = [] - total_amount = 0.0 - - for product in selected_products: - # Random amount based on item type - if product['weight_unit'] == 'piece': - amount = random.randint(1, 4) - elif product['weight_unit'] == 'dozen': - amount = 1 - elif product['weight_unit'] in ['box', 'head', 'bunch']: - amount = random.randint(1, 2) - 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(product['category'], product['organic']) - - event_products.append({ - "product_id": product['id'], - "amount": amount, - "price": price - }) - - total_amount += amount * price - - # Round total amount - total_amount = round(total_amount, 2) - - # Random notes (30% chance of having notes) - notes = None - if random.random() < 0.3: - note_options = [ - "Weekly product shopping", - "Quick lunch ingredients", - "Dinner party prep", - "Meal prep for the week", - "Emergency product run", - "Organic produce haul", - "Bulk shopping trip", - "Special occasion shopping", - "Holiday meal preparation", - "Healthy eating restart", - "Stocking up on essentials", - "Trying new recipes", - ] - notes = random.choice(note_options) - - # Create the shopping event - event_data = { - "shop_id": shop['id'], - "date": event_date.isoformat(), - "total_amount": total_amount, - "notes": notes, - "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_products)} items)") - else: - print(f" โŒ Failed to create shopping event: {response.status_code}") - if verbose: - print(f" Response: {response.text}") - except requests.exceptions.RequestException as e: - print(f" โŒ Network error creating shopping event {i+1}: {e}") - except Exception as e: - print(f" โŒ Error creating shopping event {i+1}: {e}") - - print(f" ๐Ÿ“Š Created {len(created_events)} shopping events total\n") - return created_events - -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) - - print(f"๐Ÿช Shops: {len(shops)}") - for shop in shops: - print(f" โ€ข {shop['name']} ({shop['city']})") - - print(f"\n๐Ÿฅฌ Products: {len(products)}") - categories = {} - for product in products: - category = product['category'] - if category not in categories: - categories[category] = [] - categories[category].append(product) - - for category, items in categories.items(): - organic_count = sum(1 for item in items if item['organic']) - print(f" โ€ข {category}: {len(items)} items ({organic_count} organic)") - - print(f"\n๐Ÿ›’ Shopping Events: {len(events)}") - if events: - total_spent = sum(event.get('total_amount', 0) for event in events) - avg_spent = total_spent / len(events) if events else 0 - print(f" โ€ข Total spent: ${total_spent:.2f}") - print(f" โ€ข Average per trip: ${avg_spent:.2f}") - - # Shop distribution - shop_counts = {} - for event in events: - shop_name = event['shop']['name'] - shop_counts[shop_name] = shop_counts.get(shop_name, 0) + 1 - - print(" โ€ข Events per shop:") - for shop_name, count in sorted(shop_counts.items(), key=lambda x: x[1], reverse=True): - print(f" - {shop_name}: {count} events") - -def main(): - """Main function to create all test data.""" - args = parse_arguments() - - print("๐Ÿš€ PRODUCT TRACKER TEST DATA GENERATOR") - print("=" * 50) - - if args.dry_run: - print("๐Ÿ” DRY RUN MODE - No data will be created") - - print(f"API URL: {args.url}") - print(f"Shopping events: {args.events}") - print(f"Date range: {args.days} days back") - print() - - try: - # Test connection - if not check_api_connection(args.url): - print(f"โŒ Cannot connect to the API server at {args.url}") - print(" Make sure the backend server is running!") - sys.exit(1) - - print("โœ… Connected to API server\n") - - shops = [] - products = [] - events = [] - - # Create data based on arguments - if args.shops_only: - shops = create_shops(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, 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) - 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, products, events) - - if args.dry_run: - print("\n๐Ÿ” Dry run completed. Use without --dry-run to actually create the data.") - else: - print("\n๐ŸŽ‰ Test data creation completed successfully!") - print("You can now explore the application with realistic data.") - - except KeyboardInterrupt: - print("\nโŒ Operation cancelled by user.") - sys.exit(1) - except requests.exceptions.ConnectionError: - print(f"โŒ Could not connect to the API server at {args.url}") - print(" Make sure the backend server is running!") - sys.exit(1) - except Exception as e: - print(f"โŒ Unexpected error: {e}") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/database_schema dev.drawio b/database_schema dev.drawio index 5ec0af2..e3a63dc 100644 --- a/database_schema dev.drawio +++ b/database_schema dev.drawio @@ -1,6 +1,6 @@ - + diff --git a/database_schema.drawio b/database_schema.drawio index d932033..3a76800 100644 --- a/database_schema.drawio +++ b/database_schema.drawio @@ -1,6 +1,6 @@ - + @@ -55,15 +55,15 @@ - + - + - + @@ -76,7 +76,7 @@ - + @@ -389,72 +389,140 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/ShoppingEventForm.tsx b/frontend/src/components/ShoppingEventForm.tsx index 303c2a6..85610de 100644 --- a/frontend/src/components/ShoppingEventForm.tsx +++ b/frontend/src/components/ShoppingEventForm.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Shop, Product, ShoppingEventCreate, ProductInEvent } from '../types'; import { shopApi, productApi, shoppingEventApi } from '../services/api'; @@ -30,50 +30,13 @@ const ShoppingEventForm: React.FC = () => { }); const [autoCalculate, setAutoCalculate] = useState(true); - useEffect(() => { - fetchShops(); - fetchProducts(); - if (isEditMode && id) { - fetchShoppingEvent(parseInt(id)); - } - }, [id, isEditMode]); - // 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 selectedProducts changes - useEffect(() => { - if (autoCalculate) { - const calculatedTotal = calculateTotal(selectedProducts); - setFormData(prev => ({ - ...prev, - total_amount: calculatedTotal > 0 ? calculatedTotal : undefined - })); - } - }, [selectedProducts, autoCalculate]); - - const fetchShops = async () => { - try { - const response = await shopApi.getAll(); - setShops(response.data); - } catch (error) { - console.error('Error fetching shops:', error); - } - }; - - const fetchProducts = async () => { - try { - const response = await productApi.getAll(); - setProducts(response.data); - } catch (error) { - console.error('Error fetching products:', error); - } - }; - - const fetchShoppingEvent = async (eventId: number) => { + const fetchShoppingEvent = useCallback(async (eventId: number) => { try { setLoadingEvent(true); const response = await shoppingEventApi.getById(eventId); @@ -116,6 +79,43 @@ const ShoppingEventForm: React.FC = () => { } finally { setLoadingEvent(false); } + }, []); + + useEffect(() => { + fetchShops(); + fetchProducts(); + if (isEditMode && id) { + fetchShoppingEvent(parseInt(id)); + } + }, [id, isEditMode, fetchShoppingEvent]); + + // Update total amount whenever selectedProducts changes + useEffect(() => { + if (autoCalculate) { + const calculatedTotal = calculateTotal(selectedProducts); + setFormData(prev => ({ + ...prev, + total_amount: calculatedTotal > 0 ? calculatedTotal : undefined + })); + } + }, [selectedProducts, autoCalculate]); + + const fetchShops = async () => { + try { + const response = await shopApi.getAll(); + setShops(response.data); + } catch (error) { + console.error('Error fetching shops:', error); + } + }; + + const fetchProducts = async () => { + try { + const response = await productApi.getAll(); + setProducts(response.data); + } catch (error) { + console.error('Error fetching products:', error); + } }; const addProductToEvent = () => { @@ -283,7 +283,7 @@ const ShoppingEventForm: React.FC = () => { {products.map(product => ( ))}