rename grocery to product
This commit is contained in:
parent
1b984d18d9
commit
d27871160e
@ -1,6 +1,6 @@
|
|||||||
# 🚀 Quick Start Guide
|
# 🚀 Quick Start Guide
|
||||||
|
|
||||||
Get your Grocery Tracker up and running in minutes!
|
Get your Product Tracker up and running in minutes!
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@ -62,8 +62,8 @@ This will:
|
|||||||
|
|
||||||
## First Steps
|
## First Steps
|
||||||
|
|
||||||
1. **Add a Shop**: Go to "Shops" and add your first grocery store
|
1. **Add a Shop**: Go to "Shops" and add your first store
|
||||||
2. **Add Groceries**: Go to "Groceries" and add some items
|
2. **Add Products**: Go to "Products" and add some items
|
||||||
3. **Record a Purchase**: Use "Add Purchase" to record your shopping
|
3. **Record a Purchase**: Use "Add Purchase" to record your shopping
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
@ -85,8 +85,8 @@ This will:
|
|||||||
|
|
||||||
✅ **Complete Backend**: FastAPI with SQLAlchemy and SQLite
|
✅ **Complete Backend**: FastAPI with SQLAlchemy and SQLite
|
||||||
✅ **Modern Frontend**: React with TypeScript and Tailwind CSS
|
✅ **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
|
✅ **API Documentation**: Automatic Swagger docs
|
||||||
✅ **Beautiful UI**: Responsive design with modern components
|
✅ **Beautiful UI**: Responsive design with modern components
|
||||||
|
|
||||||
Happy grocery tracking! 🛒
|
Happy product tracking! 🛒
|
||||||
48
README.md
48
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
|
## 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
|
- **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
|
- **Price Tracking**: Monitor price changes over time
|
||||||
- **Modern UI**: Clean, responsive interface built with React and Tailwind CSS
|
- **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
|
### Core Entities
|
||||||
|
|
||||||
#### Groceries (`groceries` table)
|
#### Products (`products` table)
|
||||||
- `id`: Integer, Primary key, Auto-increment
|
- `id`: Integer, Primary key, Auto-increment
|
||||||
- `name`: String, Grocery name (indexed, required)
|
- `name`: String, Product name (indexed, required)
|
||||||
- `category`: String, Food category (required)
|
- `category`: String, Food category (required)
|
||||||
- `organic`: Boolean, Organic flag (default: false)
|
- `organic`: Boolean, Organic flag (default: false)
|
||||||
- `weight`: Float, Weight/volume (optional)
|
- `weight`: Float, Weight/volume (optional)
|
||||||
@ -72,11 +72,11 @@ A web application for tracking grocery prices and shopping events. Built with Fa
|
|||||||
|
|
||||||
### Association Table
|
### Association Table
|
||||||
|
|
||||||
#### Shopping Event Groceries (`shopping_event_groceries` table)
|
#### Shopping Event Products (`shopping_event_products` table)
|
||||||
Many-to-many relationship between shopping events and groceries with additional data:
|
Many-to-many relationship between shopping events and products with additional data:
|
||||||
- `id`: Integer, Primary key, Auto-increment
|
- `id`: Integer, Primary key, Auto-increment
|
||||||
- `shopping_event_id`: Integer, Foreign key to shopping_events (required)
|
- `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)
|
- `amount`: Float, Quantity purchased in this event (required, > 0)
|
||||||
- `price`: Float, Price at time of purchase (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) │ │ │
|
│ │ │ (Association Table) │ │ │
|
||||||
│ • id │ ←──────→│ • shopping_event_id │ ←──────→│ • id │
|
│ • id │ ←──────→│ • shopping_event_id │ ←──────→│ • id │
|
||||||
│ • name │ 1:N │ • grocery_id │ N:M │ • name │
|
│ • name │ 1:N │ • product_id │ N:M │ • name │
|
||||||
│ • city │ │ • amount │ │ • category │
|
│ • city │ │ • amount │ │ • category │
|
||||||
│ • address │ │ • price │ │ • organic │
|
│ • address │ │ • price │ │ • organic │
|
||||||
│ • created_at │ │ │ │ • weight │
|
│ • created_at │ │ │ │ • weight │
|
||||||
@ -110,7 +110,7 @@ Many-to-many relationship between shopping events and groceries with additional
|
|||||||
|
|
||||||
### Key Features
|
### 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)
|
- **Flexible Quantities**: Support for decimal amounts (e.g., 1.5 kg of apples)
|
||||||
- **Auto-calculation**: Total amount can be automatically calculated from individual items
|
- **Auto-calculation**: Total amount can be automatically calculated from individual items
|
||||||
- **Free Items**: Supports items with price 0 (samples, promotions, etc.)
|
- **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:**
|
4. **Setup database:**
|
||||||
```bash
|
```bash
|
||||||
# Create PostgreSQL database
|
# Create PostgreSQL database
|
||||||
createdb grocery_tracker
|
createdb product_tracker
|
||||||
|
|
||||||
# Copy environment variables
|
# Copy environment variables
|
||||||
cp env.example .env
|
cp env.example .env
|
||||||
@ -189,12 +189,12 @@ Many-to-many relationship between shopping events and groceries with additional
|
|||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
### Groceries
|
### Products
|
||||||
- `GET /groceries/` - List all groceries
|
- `GET /products/` - List all products
|
||||||
- `POST /groceries/` - Create new grocery
|
- `POST /products/` - Create new product
|
||||||
- `GET /groceries/{id}` - Get specific grocery
|
- `GET /products/{id}` - Get specific product
|
||||||
- `PUT /groceries/{id}` - Update grocery
|
- `PUT /products/{id}` - Update product
|
||||||
- `DELETE /groceries/{id}` - Delete grocery
|
- `DELETE /products/{id}` - Delete product
|
||||||
|
|
||||||
### Shops
|
### Shops
|
||||||
- `GET /shops/` - List all shops
|
- `GET /shops/` - List all shops
|
||||||
@ -212,8 +212,8 @@ Many-to-many relationship between shopping events and groceries with additional
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
1. **Add Shops**: Start by adding shops where you buy groceries
|
1. **Add Shops**: Start by adding shops where you buy products
|
||||||
2. **Add Groceries**: Create grocery items with prices and categories
|
2. **Add Products**: Create product items with prices and categories
|
||||||
3. **Record Purchases**: Use the "Add Purchase" form to record shopping events
|
3. **Record Purchases**: Use the "Add Purchase" form to record shopping events
|
||||||
4. **Track Prices**: Monitor how prices change over time
|
4. **Track Prices**: Monitor how prices change over time
|
||||||
5. **View Statistics**: Analyze spending patterns by category and shop
|
5. **View Statistics**: Analyze spending patterns by category and shop
|
||||||
@ -252,8 +252,8 @@ services:
|
|||||||
db:
|
db:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: grocery_tracker
|
POSTGRES_DB: product_tracker
|
||||||
POSTGRES_USER: grocery_user
|
POSTGRES_USER: product_user
|
||||||
POSTGRES_PASSWORD: your_password
|
POSTGRES_PASSWORD: your_password
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
@ -267,7 +267,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql://grocery_user:your_password@db:5432/grocery_tracker
|
DATABASE_URL: postgresql://product_user:your_password@db:5432/product_tracker
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build: ./frontend
|
build: ./frontend
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
# Test Data Scripts Documentation
|
# 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
|
## Scripts Overview
|
||||||
|
|
||||||
### 1. `create_test_data.py` - Comprehensive Test Data Generator
|
### 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
|
#### Basic Usage
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ python create_test_data.py --dry-run
|
|||||||
| `--days N` | Number of days back to generate events | 90 |
|
| `--days N` | Number of days back to generate events | 90 |
|
||||||
| `--url URL` | API base URL | http://localhost:8000 |
|
| `--url URL` | API base URL | http://localhost:8000 |
|
||||||
| `--shops-only` | Create only shops | False |
|
| `--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 |
|
| `--events-only` | Create only shopping events (requires existing data) | False |
|
||||||
| `--verbose`, `-v` | Verbose output with detailed progress | False |
|
| `--verbose`, `-v` | Verbose output with detailed progress | False |
|
||||||
| `--dry-run` | Show what would be created without creating it | 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
|
# Create only shops
|
||||||
python create_test_data.py --shops-only
|
python create_test_data.py --shops-only
|
||||||
|
|
||||||
# Create only groceries
|
# Create only products
|
||||||
python create_test_data.py --groceries-only
|
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
|
python create_test_data.py --events-only --events 100
|
||||||
|
|
||||||
# Create test data for the past 6 months with verbose output
|
# Create test data for the past 6 months with verbose output
|
||||||
@ -74,14 +74,14 @@ python cleanup_test_data.py
|
|||||||
- **Safeway** (San Francisco)
|
- **Safeway** (San Francisco)
|
||||||
- **Trader Joe's** (Berkeley)
|
- **Trader Joe's** (Berkeley)
|
||||||
- **Berkeley Bowl** (Berkeley)
|
- **Berkeley Bowl** (Berkeley)
|
||||||
- **Rainbow Grocery** (San Francisco)
|
- **Rainbow Product** (San Francisco)
|
||||||
- **Mollie Stone's Market** (Palo Alto)
|
- **Mollie Stone's Market** (Palo Alto)
|
||||||
- **Costco Wholesale** (San Mateo)
|
- **Costco Wholesale** (San Mateo)
|
||||||
- **Target** (Mountain View)
|
- **Target** (Mountain View)
|
||||||
- **Sprouts Farmers Market** (Sunnyvale)
|
- **Sprouts Farmers Market** (Sunnyvale)
|
||||||
- **Lucky Supermarket** (San Jose)
|
- **Lucky Supermarket** (San Jose)
|
||||||
|
|
||||||
### Groceries (50+ items across 8 categories)
|
### Products (50+ items across 8 categories)
|
||||||
|
|
||||||
| Category | Items | Organic Options |
|
| Category | Items | Organic Options |
|
||||||
|----------|-------|-----------------|
|
|----------|-------|-----------------|
|
||||||
@ -190,7 +190,7 @@ After running the test data scripts:
|
|||||||
|
|
||||||
The test data is designed to showcase all application features:
|
The test data is designed to showcase all application features:
|
||||||
- Multiple shops and locations
|
- Multiple shops and locations
|
||||||
- Diverse grocery categories
|
- Diverse product categories
|
||||||
- Realistic shopping patterns
|
- Realistic shopping patterns
|
||||||
- Price variations and organic options
|
- Price variations and organic options
|
||||||
- Historical data for analytics
|
- 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:
|
You can modify the data arrays in `create_test_data.py` to create custom test scenarios:
|
||||||
|
|
||||||
- Add more shops for specific regions
|
- Add more shops for specific regions
|
||||||
- Include specialty grocery categories
|
- Include specialty product categories
|
||||||
- Adjust price ranges for different markets
|
- Adjust price ranges for different markets
|
||||||
- Create seasonal shopping patterns
|
- Create seasonal shopping patterns
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Script to clean up all test data from the Grocery Tracker application.
|
Script to clean up all test data from the Product Tracker application.
|
||||||
This will delete all shopping events, groceries, and shops.
|
This will delete all shopping events, products, and shops.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@ -42,37 +42,37 @@ def delete_all_shopping_events() -> int:
|
|||||||
print(f" ❌ Error fetching shopping events: {e}")
|
print(f" ❌ Error fetching shopping events: {e}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def delete_all_groceries() -> int:
|
def delete_all_products() -> int:
|
||||||
"""Delete all groceries and return the count of deleted groceries."""
|
"""Delete all products and return the count of deleted products."""
|
||||||
print("🥬 Deleting all groceries...")
|
print("🥬 Deleting all products...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get all groceries
|
# Get all products
|
||||||
response = requests.get(f"{BASE_URL}/groceries/")
|
response = requests.get(f"{BASE_URL}/products/")
|
||||||
if response.status_code != 200:
|
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
|
return 0
|
||||||
|
|
||||||
groceries = response.json()
|
products = response.json()
|
||||||
deleted_count = 0
|
deleted_count = 0
|
||||||
|
|
||||||
for grocery in groceries:
|
for product in products:
|
||||||
try:
|
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:
|
if delete_response.status_code == 200:
|
||||||
deleted_count += 1
|
deleted_count += 1
|
||||||
organic_label = "🌱" if grocery['organic'] else "🌾"
|
organic_label = "🌱" if product['organic'] else "🌾"
|
||||||
print(f" ✅ Deleted grocery: {organic_label} {grocery['name']}")
|
print(f" ✅ Deleted product: {organic_label} {product['name']}")
|
||||||
else:
|
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:
|
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
|
return deleted_count
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ❌ Error fetching groceries: {e}")
|
print(f" ❌ Error fetching products: {e}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def delete_all_shops() -> int:
|
def delete_all_shops() -> int:
|
||||||
@ -115,15 +115,15 @@ def get_current_data_summary():
|
|||||||
try:
|
try:
|
||||||
# Get counts
|
# Get counts
|
||||||
shops_response = requests.get(f"{BASE_URL}/shops/")
|
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/")
|
events_response = requests.get(f"{BASE_URL}/shopping-events/")
|
||||||
|
|
||||||
shops_count = len(shops_response.json()) if shops_response.status_code == 200 else 0
|
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
|
events_count = len(events_response.json()) if events_response.status_code == 200 else 0
|
||||||
|
|
||||||
print(f"🏪 Shops: {shops_count}")
|
print(f"🏪 Shops: {shops_count}")
|
||||||
print(f"🥬 Groceries: {groceries_count}")
|
print(f"🥬 Products: {products_count}")
|
||||||
print(f"🛒 Shopping Events: {events_count}")
|
print(f"🛒 Shopping Events: {events_count}")
|
||||||
|
|
||||||
if events_count > 0 and events_response.status_code == 200:
|
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(f"💰 Total spent: ${total_spent:.2f}")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
return shops_count, groceries_count, events_count
|
return shops_count, products_count, events_count
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error getting data summary: {e}")
|
print(f"❌ Error getting data summary: {e}")
|
||||||
@ -140,9 +140,9 @@ def get_current_data_summary():
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main function to clean up all test data."""
|
"""Main function to clean up all test data."""
|
||||||
print("🧹 GROCERY TRACKER DATA CLEANUP")
|
print("🧹 PRODUCT TRACKER DATA CLEANUP")
|
||||||
print("=" * 40)
|
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")
|
print("Make sure the backend server is running on http://localhost:8000\n")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -155,9 +155,9 @@ def main():
|
|||||||
print("✅ Connected to API server\n")
|
print("✅ Connected to API server\n")
|
||||||
|
|
||||||
# Show current data
|
# 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!")
|
print("✅ Database is already empty. Nothing to clean up!")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -171,19 +171,19 @@ def main():
|
|||||||
|
|
||||||
print("\n🧹 Starting cleanup process...\n")
|
print("\n🧹 Starting cleanup process...\n")
|
||||||
|
|
||||||
# Delete in order: events -> groceries -> shops
|
# Delete in order: events -> products -> shops
|
||||||
# (due to foreign key constraints)
|
# (due to foreign key constraints)
|
||||||
deleted_events = delete_all_shopping_events()
|
deleted_events = delete_all_shopping_events()
|
||||||
deleted_groceries = delete_all_groceries()
|
deleted_products = delete_all_products()
|
||||||
deleted_shops = delete_all_shops()
|
deleted_shops = delete_all_shops()
|
||||||
|
|
||||||
# Final summary
|
# Final summary
|
||||||
print("📋 CLEANUP SUMMARY")
|
print("📋 CLEANUP SUMMARY")
|
||||||
print("=" * 30)
|
print("=" * 30)
|
||||||
print(f"🛒 Shopping Events deleted: {deleted_events}")
|
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"🏪 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("\n🎉 Cleanup completed successfully!")
|
||||||
print("The database is now empty and ready for fresh data.")
|
print("The database is now empty and ready for fresh data.")
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Script to create comprehensive test data for the Grocery Tracker application.
|
Script to create comprehensive test data for the Product Tracker application.
|
||||||
This includes shops, groceries, and shopping events with realistic data.
|
This includes shops, products, and shopping events with realistic data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@ -20,7 +20,7 @@ SHOPS_DATA = [
|
|||||||
{"name": "Safeway", "city": "San Francisco", "address": "2020 Market St"},
|
{"name": "Safeway", "city": "San Francisco", "address": "2020 Market St"},
|
||||||
{"name": "Trader Joe's", "city": "Berkeley", "address": "1885 University Ave"},
|
{"name": "Trader Joe's", "city": "Berkeley", "address": "1885 University Ave"},
|
||||||
{"name": "Berkeley Bowl", "city": "Berkeley", "address": "2020 Oregon St"},
|
{"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": "Mollie Stone's Market", "city": "Palo Alto", "address": "164 S California Ave"},
|
||||||
{"name": "Costco Wholesale", "city": "San Mateo", "address": "2300 S Norfolk St"},
|
{"name": "Costco Wholesale", "city": "San Mateo", "address": "2300 S Norfolk St"},
|
||||||
{"name": "Target", "city": "Mountain View", "address": "1200 El Camino Real"},
|
{"name": "Target", "city": "Mountain View", "address": "1200 El Camino Real"},
|
||||||
@ -119,13 +119,13 @@ PRICE_RANGES = {
|
|||||||
|
|
||||||
def parse_arguments():
|
def parse_arguments():
|
||||||
"""Parse command line 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('--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('--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('--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('--shops-only', action='store_true', help='Create only shops')
|
||||||
parser.add_argument('--groceries-only', action='store_true', help='Create only 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 groceries)')
|
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('--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')
|
parser.add_argument('--dry-run', action='store_true', help='Show what would be created without actually creating it')
|
||||||
return parser.parse_args()
|
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")
|
print(f" 📊 Created {len(created_shops)} shops total\n")
|
||||||
return created_shops
|
return created_shops
|
||||||
|
|
||||||
def create_groceries(base_url: str, verbose: bool = False, dry_run: bool = False) -> List[Dict[str, Any]]:
|
def create_products(base_url: str, verbose: bool = False, dry_run: bool = False) -> List[Dict[str, Any]]:
|
||||||
"""Create groceries and return the created grocery objects."""
|
"""Create products and return the created product objects."""
|
||||||
print("🥬 Creating groceries...")
|
print("🥬 Creating products...")
|
||||||
created_groceries = []
|
created_products = []
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
print(" [DRY RUN] Would create the following groceries:")
|
print(" [DRY RUN] Would create the following products:")
|
||||||
for grocery_data in GROCERIES_DATA:
|
for product_data in GROCERIES_DATA:
|
||||||
organic_label = "🌱" if grocery_data['organic'] else "🌾"
|
organic_label = "🌱" if product_data['organic'] else "🌾"
|
||||||
print(f" 📋 {organic_label} {grocery_data['name']} ({grocery_data['category']})")
|
print(f" 📋 {organic_label} {product_data['name']} ({product_data['category']})")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
for grocery_data in GROCERIES_DATA:
|
for product_data in GROCERIES_DATA:
|
||||||
try:
|
try:
|
||||||
if verbose:
|
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:
|
if response.status_code == 200:
|
||||||
grocery = response.json()
|
product = response.json()
|
||||||
created_groceries.append(grocery)
|
created_products.append(product)
|
||||||
organic_label = "🌱" if grocery['organic'] else "🌾"
|
organic_label = "🌱" if product['organic'] else "🌾"
|
||||||
print(f" ✅ Created grocery: {organic_label} {grocery['name']} ({grocery['category']})")
|
print(f" ✅ Created product: {organic_label} {product['name']} ({product['category']})")
|
||||||
else:
|
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:
|
if verbose:
|
||||||
print(f" Response: {response.text}")
|
print(f" Response: {response.text}")
|
||||||
except requests.exceptions.RequestException as e:
|
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:
|
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")
|
print(f" 📊 Created {len(created_products)} products total\n")
|
||||||
return created_groceries
|
return created_products
|
||||||
|
|
||||||
def generate_random_price(category: str, organic: bool = False) -> float:
|
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))
|
min_price, max_price = PRICE_RANGES.get(category, (1.99, 9.99))
|
||||||
|
|
||||||
# Organic items are typically 20-50% more expensive
|
# 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)
|
return round(price, 2)
|
||||||
|
|
||||||
def get_existing_data(base_url: str) -> tuple[List[Dict], List[Dict]]:
|
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:
|
try:
|
||||||
shops_response = requests.get(f"{base_url}/shops/", timeout=10)
|
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 []
|
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:
|
except requests.exceptions.RequestException as e:
|
||||||
print(f" ❌ Error fetching existing data: {e}")
|
print(f" ❌ Error fetching existing data: {e}")
|
||||||
return [], []
|
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,
|
num_events: int = 25, days_back: int = 90,
|
||||||
verbose: bool = False, dry_run: bool = False) -> List[Dict[str, Any]]:
|
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...")
|
print(f"🛒 Creating {num_events} shopping events...")
|
||||||
created_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.")
|
print(" ❌ No shops available. Cannot create shopping events.")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if not groceries:
|
if not products:
|
||||||
print(" ❌ No groceries available. Cannot create shopping events.")
|
print(" ❌ No products available. Cannot create shopping events.")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Generate events over the specified time period
|
# 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" [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" 📋 Date range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
|
||||||
print(f" 📋 Available shops: {len(shops)}")
|
print(f" 📋 Available shops: {len(shops)}")
|
||||||
print(f" 📋 Available groceries: {len(groceries)}")
|
print(f" 📋 Available products: {len(products)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
for i in range(num_events):
|
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)
|
minutes=random.randint(0, 59)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Random number of groceries (2-8 items per shopping trip)
|
# Random number of products (2-8 items per shopping trip)
|
||||||
num_groceries = random.randint(2, 8)
|
num_products = random.randint(2, 8)
|
||||||
selected_groceries = random.sample(groceries, min(num_groceries, len(groceries)))
|
selected_products = random.sample(products, min(num_products, len(products)))
|
||||||
|
|
||||||
# Create grocery items for this event
|
# Create product items for this event
|
||||||
event_groceries = []
|
event_products = []
|
||||||
total_amount = 0.0
|
total_amount = 0.0
|
||||||
|
|
||||||
for grocery in selected_groceries:
|
for product in selected_products:
|
||||||
# Random amount based on item type
|
# Random amount based on item type
|
||||||
if grocery['weight_unit'] == 'piece':
|
if product['weight_unit'] == 'piece':
|
||||||
amount = random.randint(1, 4)
|
amount = random.randint(1, 4)
|
||||||
elif grocery['weight_unit'] == 'dozen':
|
elif product['weight_unit'] == 'dozen':
|
||||||
amount = 1
|
amount = 1
|
||||||
elif grocery['weight_unit'] in ['box', 'head', 'bunch']:
|
elif product['weight_unit'] in ['box', 'head', 'bunch']:
|
||||||
amount = random.randint(1, 2)
|
amount = random.randint(1, 2)
|
||||||
elif grocery['weight_unit'] in ['gallon', 'l']:
|
elif product['weight_unit'] in ['gallon', 'l']:
|
||||||
amount = 1
|
amount = 1
|
||||||
else:
|
else:
|
||||||
amount = round(random.uniform(0.5, 3.0), 2)
|
amount = round(random.uniform(0.5, 3.0), 2)
|
||||||
|
|
||||||
# Generate price based on category and organic status
|
# 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({
|
event_products.append({
|
||||||
"grocery_id": grocery['id'],
|
"product_id": product['id'],
|
||||||
"amount": amount,
|
"amount": amount,
|
||||||
"price": price
|
"price": price
|
||||||
})
|
})
|
||||||
@ -311,11 +311,11 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s
|
|||||||
notes = None
|
notes = None
|
||||||
if random.random() < 0.3:
|
if random.random() < 0.3:
|
||||||
note_options = [
|
note_options = [
|
||||||
"Weekly grocery shopping",
|
"Weekly product shopping",
|
||||||
"Quick lunch ingredients",
|
"Quick lunch ingredients",
|
||||||
"Dinner party prep",
|
"Dinner party prep",
|
||||||
"Meal prep for the week",
|
"Meal prep for the week",
|
||||||
"Emergency grocery run",
|
"Emergency product run",
|
||||||
"Organic produce haul",
|
"Organic produce haul",
|
||||||
"Bulk shopping trip",
|
"Bulk shopping trip",
|
||||||
"Special occasion shopping",
|
"Special occasion shopping",
|
||||||
@ -332,14 +332,14 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s
|
|||||||
"date": event_date.isoformat(),
|
"date": event_date.isoformat(),
|
||||||
"total_amount": total_amount,
|
"total_amount": total_amount,
|
||||||
"notes": notes,
|
"notes": notes,
|
||||||
"groceries": event_groceries
|
"products": event_products
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(f"{base_url}/shopping-events/", json=event_data, timeout=15)
|
response = requests.post(f"{base_url}/shopping-events/", json=event_data, timeout=15)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
event = response.json()
|
event = response.json()
|
||||||
created_events.append(event)
|
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:
|
else:
|
||||||
print(f" ❌ Failed to create shopping event: {response.status_code}")
|
print(f" ❌ Failed to create shopping event: {response.status_code}")
|
||||||
if verbose:
|
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")
|
print(f" 📊 Created {len(created_events)} shopping events total\n")
|
||||||
return created_events
|
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 a summary of the created test data."""
|
||||||
print("📋 TEST DATA SUMMARY")
|
print("📋 TEST DATA SUMMARY")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
@ -361,13 +361,13 @@ def print_summary(shops: List[Dict], groceries: List[Dict], events: List[Dict]):
|
|||||||
for shop in shops:
|
for shop in shops:
|
||||||
print(f" • {shop['name']} ({shop['city']})")
|
print(f" • {shop['name']} ({shop['city']})")
|
||||||
|
|
||||||
print(f"\n🥬 Groceries: {len(groceries)}")
|
print(f"\n🥬 Products: {len(products)}")
|
||||||
categories = {}
|
categories = {}
|
||||||
for grocery in groceries:
|
for product in products:
|
||||||
category = grocery['category']
|
category = product['category']
|
||||||
if category not in categories:
|
if category not in categories:
|
||||||
categories[category] = []
|
categories[category] = []
|
||||||
categories[category].append(grocery)
|
categories[category].append(product)
|
||||||
|
|
||||||
for category, items in categories.items():
|
for category, items in categories.items():
|
||||||
organic_count = sum(1 for item in items if item['organic'])
|
organic_count = sum(1 for item in items if item['organic'])
|
||||||
@ -394,7 +394,7 @@ def main():
|
|||||||
"""Main function to create all test data."""
|
"""Main function to create all test data."""
|
||||||
args = parse_arguments()
|
args = parse_arguments()
|
||||||
|
|
||||||
print("🚀 GROCERY TRACKER TEST DATA GENERATOR")
|
print("🚀 PRODUCT TRACKER TEST DATA GENERATOR")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
@ -415,28 +415,28 @@ def main():
|
|||||||
print("✅ Connected to API server\n")
|
print("✅ Connected to API server\n")
|
||||||
|
|
||||||
shops = []
|
shops = []
|
||||||
groceries = []
|
products = []
|
||||||
events = []
|
events = []
|
||||||
|
|
||||||
# Create data based on arguments
|
# Create data based on arguments
|
||||||
if args.shops_only:
|
if args.shops_only:
|
||||||
shops = create_shops(args.url, args.verbose, args.dry_run)
|
shops = create_shops(args.url, args.verbose, args.dry_run)
|
||||||
elif args.groceries_only:
|
elif args.products_only:
|
||||||
groceries = create_groceries(args.url, args.verbose, args.dry_run)
|
products = create_products(args.url, args.verbose, args.dry_run)
|
||||||
elif args.events_only:
|
elif args.events_only:
|
||||||
# Get existing data for events
|
# Get existing data for events
|
||||||
shops, groceries = get_existing_data(args.url)
|
shops, products = get_existing_data(args.url)
|
||||||
events = create_shopping_events(shops, groceries, args.url, args.events, args.days, args.verbose, args.dry_run)
|
events = create_shopping_events(shops, products, args.url, args.events, args.days, args.verbose, args.dry_run)
|
||||||
else:
|
else:
|
||||||
# Create all data
|
# Create all data
|
||||||
shops = create_shops(args.url, args.verbose, args.dry_run)
|
shops = create_shops(args.url, args.verbose, args.dry_run)
|
||||||
groceries = create_groceries(args.url, args.verbose, args.dry_run)
|
products = create_products(args.url, args.verbose, args.dry_run)
|
||||||
if shops and groceries:
|
if shops and products:
|
||||||
events = create_shopping_events(shops, groceries, args.url, args.events, args.days, args.verbose, args.dry_run)
|
events = create_shopping_events(shops, products, args.url, args.events, args.days, args.verbose, args.dry_run)
|
||||||
|
|
||||||
# Print summary
|
# Print summary
|
||||||
if not args.dry_run:
|
if not args.dry_run:
|
||||||
print_summary(shops, groceries, events)
|
print_summary(shops, products, events)
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
print("\n🔍 Dry run completed. Use without --dry-run to actually create the data.")
|
print("\n🔍 Dry run completed. Use without --dry-run to actually create the data.")
|
||||||
|
|||||||
@ -11,7 +11,7 @@ DATABASE_URL = os.getenv("DATABASE_URL")
|
|||||||
|
|
||||||
if not DATABASE_URL:
|
if not DATABASE_URL:
|
||||||
# Default to SQLite for development if no PostgreSQL URL is provided
|
# 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")
|
print("🔄 Using SQLite database for development")
|
||||||
else:
|
else:
|
||||||
print(f"🐘 Using PostgreSQL database")
|
print(f"🐘 Using PostgreSQL database")
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
# Database Configuration
|
# Database Configuration
|
||||||
# Option 1: PostgreSQL (for production)
|
# 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)
|
# 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)
|
# Authentication (optional for basic setup)
|
||||||
SECRET_KEY=your-secret-key-here
|
SECRET_KEY=your-secret-key-here
|
||||||
|
|||||||
146
backend/main.py
146
backend/main.py
@ -10,8 +10,8 @@ from database import engine, get_db
|
|||||||
models.Base.metadata.create_all(bind=engine)
|
models.Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Grocery Tracker API",
|
title="Product Tracker API",
|
||||||
description="API for tracking grocery prices and shopping events",
|
description="API for tracking product prices and shopping events",
|
||||||
version="1.0.0"
|
version="1.0.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -25,22 +25,22 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> schemas.ShoppingEventResponse:
|
def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> schemas.ShoppingEventResponse:
|
||||||
"""Build a shopping event response with groceries from the association table"""
|
"""Build a shopping event response with products from the association table"""
|
||||||
# Get groceries with their event-specific data
|
# Get products with their event-specific data
|
||||||
grocery_data = db.execute(
|
product_data = db.execute(
|
||||||
text("""
|
text("""
|
||||||
SELECT g.id, g.name, g.category, g.organic, g.weight, g.weight_unit,
|
SELECT p.id, p.name, p.category, p.organic, p.weight, p.weight_unit,
|
||||||
seg.amount, seg.price
|
sep.amount, sep.price
|
||||||
FROM groceries g
|
FROM products p
|
||||||
JOIN shopping_event_groceries seg ON g.id = seg.grocery_id
|
JOIN shopping_event_products sep ON p.id = sep.product_id
|
||||||
WHERE seg.shopping_event_id = :event_id
|
WHERE sep.shopping_event_id = :event_id
|
||||||
"""),
|
"""),
|
||||||
{"event_id": event.id}
|
{"event_id": event.id}
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
# Convert to GroceryWithEventData objects
|
# Convert to ProductWithEventData objects
|
||||||
groceries_with_data = [
|
products_with_data = [
|
||||||
schemas.GroceryWithEventData(
|
schemas.ProductWithEventData(
|
||||||
id=row.id,
|
id=row.id,
|
||||||
name=row.name,
|
name=row.name,
|
||||||
category=row.category,
|
category=row.category,
|
||||||
@ -50,7 +50,7 @@ def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> s
|
|||||||
amount=row.amount,
|
amount=row.amount,
|
||||||
price=row.price
|
price=row.price
|
||||||
)
|
)
|
||||||
for row in grocery_data
|
for row in product_data
|
||||||
]
|
]
|
||||||
|
|
||||||
return schemas.ShoppingEventResponse(
|
return schemas.ShoppingEventResponse(
|
||||||
@ -61,58 +61,58 @@ def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> s
|
|||||||
notes=event.notes,
|
notes=event.notes,
|
||||||
created_at=event.created_at,
|
created_at=event.created_at,
|
||||||
shop=event.shop,
|
shop=event.shop,
|
||||||
groceries=groceries_with_data
|
products=products_with_data
|
||||||
)
|
)
|
||||||
|
|
||||||
# Root endpoint
|
# Root endpoint
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def read_root():
|
def read_root():
|
||||||
return {"message": "Grocery Tracker API", "version": "1.0.0"}
|
return {"message": "Product Tracker API", "version": "1.0.0"}
|
||||||
|
|
||||||
# Grocery endpoints
|
# Product endpoints
|
||||||
@app.post("/groceries/", response_model=schemas.Grocery)
|
@app.post("/products/", response_model=schemas.Product)
|
||||||
def create_grocery(grocery: schemas.GroceryCreate, db: Session = Depends(get_db)):
|
def create_product(product: schemas.ProductCreate, db: Session = Depends(get_db)):
|
||||||
db_grocery = models.Grocery(**grocery.dict())
|
db_product = models.Product(**product.dict())
|
||||||
db.add(db_grocery)
|
db.add(db_product)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_grocery)
|
db.refresh(db_product)
|
||||||
return db_grocery
|
return db_product
|
||||||
|
|
||||||
@app.get("/groceries/", response_model=List[schemas.Grocery])
|
@app.get("/products/", response_model=List[schemas.Product])
|
||||||
def read_groceries(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
def read_products(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||||
groceries = db.query(models.Grocery).offset(skip).limit(limit).all()
|
products = db.query(models.Product).offset(skip).limit(limit).all()
|
||||||
return groceries
|
return products
|
||||||
|
|
||||||
@app.get("/groceries/{grocery_id}", response_model=schemas.Grocery)
|
@app.get("/products/{product_id}", response_model=schemas.Product)
|
||||||
def read_grocery(grocery_id: int, db: Session = Depends(get_db)):
|
def read_product(product_id: int, db: Session = Depends(get_db)):
|
||||||
grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first()
|
product = db.query(models.Product).filter(models.Product.id == product_id).first()
|
||||||
if grocery is None:
|
if product is None:
|
||||||
raise HTTPException(status_code=404, detail="Grocery not found")
|
raise HTTPException(status_code=404, detail="Product not found")
|
||||||
return grocery
|
return product
|
||||||
|
|
||||||
@app.put("/groceries/{grocery_id}", response_model=schemas.Grocery)
|
@app.put("/products/{product_id}", response_model=schemas.Product)
|
||||||
def update_grocery(grocery_id: int, grocery_update: schemas.GroceryUpdate, db: Session = Depends(get_db)):
|
def update_product(product_id: int, product_update: schemas.ProductUpdate, db: Session = Depends(get_db)):
|
||||||
grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first()
|
product = db.query(models.Product).filter(models.Product.id == product_id).first()
|
||||||
if grocery is None:
|
if product is None:
|
||||||
raise HTTPException(status_code=404, detail="Grocery not found")
|
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():
|
for field, value in update_data.items():
|
||||||
setattr(grocery, field, value)
|
setattr(product, field, value)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(grocery)
|
db.refresh(product)
|
||||||
return grocery
|
return product
|
||||||
|
|
||||||
@app.delete("/groceries/{grocery_id}")
|
@app.delete("/products/{product_id}")
|
||||||
def delete_grocery(grocery_id: int, db: Session = Depends(get_db)):
|
def delete_product(product_id: int, db: Session = Depends(get_db)):
|
||||||
grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first()
|
product = db.query(models.Product).filter(models.Product.id == product_id).first()
|
||||||
if grocery is None:
|
if product is None:
|
||||||
raise HTTPException(status_code=404, detail="Grocery not found")
|
raise HTTPException(status_code=404, detail="Product not found")
|
||||||
|
|
||||||
db.delete(grocery)
|
db.delete(product)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "Grocery deleted successfully"}
|
return {"message": "Product deleted successfully"}
|
||||||
|
|
||||||
# Shop endpoints
|
# Shop endpoints
|
||||||
@app.post("/shops/", response_model=schemas.Shop)
|
@app.post("/shops/", response_model=schemas.Shop)
|
||||||
@ -178,19 +178,19 @@ def create_shopping_event(event: schemas.ShoppingEventCreate, db: Session = Depe
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_event)
|
db.refresh(db_event)
|
||||||
|
|
||||||
# Add groceries to the event
|
# Add products to the event
|
||||||
for grocery_item in event.groceries:
|
for product_item in event.products:
|
||||||
grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_item.grocery_id).first()
|
product = db.query(models.Product).filter(models.Product.id == product_item.product_id).first()
|
||||||
if grocery is None:
|
if product is None:
|
||||||
raise HTTPException(status_code=404, detail=f"Grocery with id {grocery_item.grocery_id} not found")
|
raise HTTPException(status_code=404, detail=f"Product with id {product_item.product_id} not found")
|
||||||
|
|
||||||
# Insert into association table
|
# Insert into association table
|
||||||
db.execute(
|
db.execute(
|
||||||
models.shopping_event_groceries.insert().values(
|
models.shopping_event_products.insert().values(
|
||||||
shopping_event_id=db_event.id,
|
shopping_event_id=db_event.id,
|
||||||
grocery_id=grocery_item.grocery_id,
|
product_id=product_item.product_id,
|
||||||
amount=grocery_item.amount,
|
amount=product_item.amount,
|
||||||
price=grocery_item.price
|
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.total_amount = event_update.total_amount
|
||||||
event.notes = event_update.notes
|
event.notes = event_update.notes
|
||||||
|
|
||||||
# Remove existing grocery associations
|
# Remove existing product associations
|
||||||
db.execute(
|
db.execute(
|
||||||
models.shopping_event_groceries.delete().where(
|
models.shopping_event_products.delete().where(
|
||||||
models.shopping_event_groceries.c.shopping_event_id == event_id
|
models.shopping_event_products.c.shopping_event_id == event_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add new grocery associations
|
# Add new product associations
|
||||||
for grocery_item in event_update.groceries:
|
for product_item in event_update.products:
|
||||||
grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_item.grocery_id).first()
|
product = db.query(models.Product).filter(models.Product.id == product_item.product_id).first()
|
||||||
if grocery is None:
|
if product is None:
|
||||||
raise HTTPException(status_code=404, detail=f"Grocery with id {grocery_item.grocery_id} not found")
|
raise HTTPException(status_code=404, detail=f"Product with id {product_item.product_id} not found")
|
||||||
|
|
||||||
# Insert into association table
|
# Insert into association table
|
||||||
db.execute(
|
db.execute(
|
||||||
models.shopping_event_groceries.insert().values(
|
models.shopping_event_products.insert().values(
|
||||||
shopping_event_id=event_id,
|
shopping_event_id=event_id,
|
||||||
grocery_id=grocery_item.grocery_id,
|
product_id=product_item.product_id,
|
||||||
amount=grocery_item.amount,
|
amount=product_item.amount,
|
||||||
price=grocery_item.price
|
price=product_item.price
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -261,10 +261,10 @@ def delete_shopping_event(event_id: int, db: Session = Depends(get_db)):
|
|||||||
if event is None:
|
if event is None:
|
||||||
raise HTTPException(status_code=404, detail="Shopping event not found")
|
raise HTTPException(status_code=404, detail="Shopping event not found")
|
||||||
|
|
||||||
# Delete grocery associations first
|
# Delete product associations first
|
||||||
db.execute(
|
db.execute(
|
||||||
models.shopping_event_groceries.delete().where(
|
models.shopping_event_products.delete().where(
|
||||||
models.shopping_event_groceries.c.shopping_event_id == event_id
|
models.shopping_event_products.c.shopping_event_id == event_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -6,19 +6,19 @@ from datetime import datetime
|
|||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
# Association table for many-to-many relationship between shopping events and groceries
|
# Association table for many-to-many relationship between shopping events and products
|
||||||
shopping_event_groceries = Table(
|
shopping_event_products = Table(
|
||||||
'shopping_event_groceries',
|
'shopping_event_products',
|
||||||
Base.metadata,
|
Base.metadata,
|
||||||
Column('id', Integer, primary_key=True, autoincrement=True), # Artificial primary key
|
Column('id', Integer, primary_key=True, autoincrement=True), # Artificial primary key
|
||||||
Column('shopping_event_id', Integer, ForeignKey('shopping_events.id'), nullable=False),
|
Column('shopping_event_id', Integer, ForeignKey('shopping_events.id'), nullable=False),
|
||||||
Column('grocery_id', Integer, ForeignKey('groceries.id'), nullable=False),
|
Column('product_id', Integer, ForeignKey('products.id'), nullable=False),
|
||||||
Column('amount', Float, nullable=False), # Amount of this grocery bought in this event
|
Column('amount', Float, nullable=False), # Amount of this product bought in this event
|
||||||
Column('price', Float, nullable=False) # Price of this grocery at the time of this shopping event
|
Column('price', Float, nullable=False) # Price of this product at the time of this shopping event
|
||||||
)
|
)
|
||||||
|
|
||||||
class Grocery(Base):
|
class Product(Base):
|
||||||
__tablename__ = "groceries"
|
__tablename__ = "products"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
name = Column(String, nullable=False, 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())
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
# Relationships
|
# 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):
|
class Shop(Base):
|
||||||
__tablename__ = "shops"
|
__tablename__ = "shops"
|
||||||
@ -58,4 +58,4 @@ class ShoppingEvent(Base):
|
|||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
shop = relationship("Shop", back_populates="shopping_events")
|
shop = relationship("Shop", back_populates="shopping_events")
|
||||||
groceries = relationship("Grocery", secondary=shopping_event_groceries, back_populates="shopping_events")
|
products = relationship("Product", secondary=shopping_event_products, back_populates="shopping_events")
|
||||||
@ -22,7 +22,7 @@ def main():
|
|||||||
backend_dir = Path(__file__).parent
|
backend_dir = Path(__file__).parent
|
||||||
os.chdir(backend_dir)
|
os.chdir(backend_dir)
|
||||||
|
|
||||||
print("🍃 Starting Grocery Tracker Backend Development Server")
|
print("🍃 Starting Product Tracker Backend Development Server")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
# Check if virtual environment exists
|
# Check if virtual environment exists
|
||||||
|
|||||||
@ -3,24 +3,24 @@ from typing import Optional, List
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Base schemas
|
# Base schemas
|
||||||
class GroceryBase(BaseModel):
|
class ProductBase(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
category: str
|
category: str
|
||||||
organic: bool = False
|
organic: bool = False
|
||||||
weight: Optional[float] = None
|
weight: Optional[float] = None
|
||||||
weight_unit: str = "g"
|
weight_unit: str = "g"
|
||||||
|
|
||||||
class GroceryCreate(GroceryBase):
|
class ProductCreate(ProductBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class GroceryUpdate(BaseModel):
|
class ProductUpdate(BaseModel):
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
category: Optional[str] = None
|
category: Optional[str] = None
|
||||||
organic: Optional[bool] = None
|
organic: Optional[bool] = None
|
||||||
weight: Optional[float] = None
|
weight: Optional[float] = None
|
||||||
weight_unit: Optional[str] = None
|
weight_unit: Optional[str] = None
|
||||||
|
|
||||||
class Grocery(GroceryBase):
|
class Product(ProductBase):
|
||||||
id: int
|
id: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
@ -51,12 +51,12 @@ class Shop(ShopBase):
|
|||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
# Shopping Event schemas
|
# Shopping Event schemas
|
||||||
class GroceryInEvent(BaseModel):
|
class ProductInEvent(BaseModel):
|
||||||
grocery_id: int
|
product_id: int
|
||||||
amount: float = Field(..., gt=0)
|
amount: float = Field(..., gt=0)
|
||||||
price: float = Field(..., ge=0) # Price at the time of this shopping event (allow free items)
|
price: float = Field(..., ge=0) # Price at the time of this shopping event (allow free items)
|
||||||
|
|
||||||
class GroceryWithEventData(BaseModel):
|
class ProductWithEventData(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
category: str
|
category: str
|
||||||
@ -76,21 +76,21 @@ class ShoppingEventBase(BaseModel):
|
|||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
class ShoppingEventCreate(ShoppingEventBase):
|
class ShoppingEventCreate(ShoppingEventBase):
|
||||||
groceries: List[GroceryInEvent] = []
|
products: List[ProductInEvent] = []
|
||||||
|
|
||||||
class ShoppingEventUpdate(BaseModel):
|
class ShoppingEventUpdate(BaseModel):
|
||||||
shop_id: Optional[int] = None
|
shop_id: Optional[int] = None
|
||||||
date: Optional[datetime] = None
|
date: Optional[datetime] = None
|
||||||
total_amount: Optional[float] = Field(None, ge=0)
|
total_amount: Optional[float] = Field(None, ge=0)
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
groceries: Optional[List[GroceryInEvent]] = None
|
products: Optional[List[ProductInEvent]] = None
|
||||||
|
|
||||||
class ShoppingEventResponse(ShoppingEventBase):
|
class ShoppingEventResponse(ShoppingEventBase):
|
||||||
id: int
|
id: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
shop: Shop
|
shop: Shop
|
||||||
groceries: List[GroceryWithEventData] = []
|
products: List[ProductWithEventData] = []
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|||||||
603
database_schema dev.drawio
Normal file
603
database_schema dev.drawio
Normal file
@ -0,0 +1,603 @@
|
|||||||
|
<mxfile host="65bd71144e">
|
||||||
|
<diagram name="Product Tracker Database Schema" id="database-schema">
|
||||||
|
<mxGraphModel dx="999" dy="529" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0"/>
|
||||||
|
<mxCell id="1" parent="0"/>
|
||||||
|
<mxCell id="shop-event-relation" value="" style="endArrow=ERmany;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=ERone;startFill=0;endFill=0;" parent="1" source="71" target="43" edge="1">
|
||||||
|
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||||
|
<mxPoint x="300" y="470" as="sourcePoint"/>
|
||||||
|
<mxPoint x="350" y="420" as="targetPoint"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="shop-event-label" value="1:N" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="960" y="399" width="40" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="event-association-relation" value="" style="endArrow=ERmany;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=ERone;startFill=0;endFill=0;" parent="1" source="40" target="99" edge="1">
|
||||||
|
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||||
|
<mxPoint x="620" y="310" as="sourcePoint"/>
|
||||||
|
<mxPoint x="720" y="270" as="targetPoint"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="event-association-label" value="1:N" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="900" y="230" width="40" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="product-association-relation" value="" style="endArrow=ERmany;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=ERone;startFill=0;endFill=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="161" target="102" edge="1">
|
||||||
|
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||||
|
<mxPoint x="1020" y="550" as="sourcePoint"/>
|
||||||
|
<mxPoint x="720" y="290" as="targetPoint"/>
|
||||||
|
<Array as="points"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="product-association-label" value="1:N" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="390" y="160" width="40" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="diagram-title" value="Product Tracker Database Schema" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=20;fontStyle=1;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="400" y="20" width="320" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="2" value="<span style="color: rgb(0, 0, 0); text-wrap-mode: wrap;">products</span>" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="390" y="440" width="180" height="180" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="3" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="2" vertex="1">
|
||||||
|
<mxGeometry y="30" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="4" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="3" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="5" value="id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" parent="3" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="6" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="2" vertex="1">
|
||||||
|
<mxGeometry y="60" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="7" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="6" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="8" value="name: STRING" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="6" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="144" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="2" vertex="1">
|
||||||
|
<mxGeometry y="90" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="145" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="144" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="146" value="category_id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="144" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="21" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="2" vertex="1">
|
||||||
|
<mxGeometry y="120" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="22" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="21" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="23" value="created_at: DATETIME" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="21" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="15" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="2" vertex="1">
|
||||||
|
<mxGeometry y="150" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="16" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="15" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="17" value="updated_at: DATETIME" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="15" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="39" value="<span style="color: rgb(0, 0, 0); text-wrap-mode: wrap;">shopping_events</span>" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="580" y="150" width="180" height="240" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="40" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="39" vertex="1">
|
||||||
|
<mxGeometry y="30" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="41" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="40" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="42" value="id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" parent="40" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="43" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="39" vertex="1">
|
||||||
|
<mxGeometry y="60" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="44" value="FK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="43" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="45" value="shop_id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="43" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="46" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="39" vertex="1">
|
||||||
|
<mxGeometry y="90" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="47" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="46" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="48" value="date: DATETIME" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="46" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="49" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="39" vertex="1">
|
||||||
|
<mxGeometry y="120" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="50" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="49" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="51" value="total_amount: FLOAT" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="49" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="52" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="39" vertex="1">
|
||||||
|
<mxGeometry y="150" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="53" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="52" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="54" value="notes: STRING" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="52" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="58" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="39" vertex="1">
|
||||||
|
<mxGeometry y="180" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="59" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="58" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="60" value="created_at: DATETIME" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="58" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="111" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="39" vertex="1">
|
||||||
|
<mxGeometry y="210" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="112" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="111" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="113" value="updated_at: DATETIME" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="111" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="70" value="<span style="color: rgb(0, 0, 0); text-wrap-mode: wrap;">shops</span>" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="80" y="120" width="180" height="210" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="71" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="70" vertex="1">
|
||||||
|
<mxGeometry y="30" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="72" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="71" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="73" value="id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" parent="71" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="74" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="70" vertex="1">
|
||||||
|
<mxGeometry y="60" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="75" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="74" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="76" value="name: STRING" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="74" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="77" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="70" vertex="1">
|
||||||
|
<mxGeometry y="90" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="78" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="77" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="79" value="city: STRING" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="77" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="80" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="70" vertex="1">
|
||||||
|
<mxGeometry y="120" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="81" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="80" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="82" value="address: STRING" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="80" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="89" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="70" vertex="1">
|
||||||
|
<mxGeometry y="150" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="90" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="89" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="91" value="created_at: DATETIME" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="89" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="92" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;strokeColor=#b85450;" parent="70" vertex="1">
|
||||||
|
<mxGeometry y="180" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="93" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="92" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="94" value="updated_at: DATETIME" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="92" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="95" value="<span style="color: rgb(0, 0, 0); text-wrap-mode: wrap;">shopping_event_products</span>" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="1090" y="260" width="240" height="180" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="96" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="95" vertex="1">
|
||||||
|
<mxGeometry y="30" width="240" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="97" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="96" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="98" value="id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" parent="96" vertex="1">
|
||||||
|
<mxGeometry x="30" width="210" height="30" as="geometry">
|
||||||
|
<mxRectangle width="210" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="99" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="95" vertex="1">
|
||||||
|
<mxGeometry y="60" width="240" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="100" value="FK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="99" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="101" value="shopping_event_id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="99" vertex="1">
|
||||||
|
<mxGeometry x="30" width="210" height="30" as="geometry">
|
||||||
|
<mxRectangle width="210" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="102" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="95" vertex="1">
|
||||||
|
<mxGeometry y="90" width="240" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="103" value="FK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="102" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="104" value="product_id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="102" vertex="1">
|
||||||
|
<mxGeometry x="30" width="210" height="30" as="geometry">
|
||||||
|
<mxRectangle width="210" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="105" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="95" vertex="1">
|
||||||
|
<mxGeometry y="120" width="240" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="106" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="105" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="107" value="amount: FLOAT" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="105" vertex="1">
|
||||||
|
<mxGeometry x="30" width="210" height="30" as="geometry">
|
||||||
|
<mxRectangle width="210" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="108" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="95" vertex="1">
|
||||||
|
<mxGeometry y="150" width="240" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="109" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="108" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="110" value="price: FLOAT" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="108" vertex="1">
|
||||||
|
<mxGeometry x="30" width="210" height="30" as="geometry">
|
||||||
|
<mxRectangle width="210" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="119" value="<span style="color: rgb(0, 0, 0); text-wrap-mode: wrap;">grocerie_categories</span>" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="80" y="440" width="180" height="150" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="120" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="119" vertex="1">
|
||||||
|
<mxGeometry y="30" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="121" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="120" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="122" value="id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" parent="120" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="123" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="119" vertex="1">
|
||||||
|
<mxGeometry y="60" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="124" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="123" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="125" value="name: STRING" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="123" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="138" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="119" vertex="1">
|
||||||
|
<mxGeometry y="90" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="139" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="138" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="140" value="created_at: DATETIME" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="138" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="141" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="119" vertex="1">
|
||||||
|
<mxGeometry y="120" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="142" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="141" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="143" value="updated_at: DATETIME" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="141" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="160" value="<span style="color: rgb(0, 0, 0); text-wrap-mode: wrap;">products</span>" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="710" y="445" width="180" height="300" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="161" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="160" vertex="1">
|
||||||
|
<mxGeometry y="30" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="162" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="161" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="163" value="id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" parent="161" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="164" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="160" vertex="1">
|
||||||
|
<mxGeometry y="60" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="165" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="164" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="166" value="name: STRING" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="164" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="170" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="160" vertex="1">
|
||||||
|
<mxGeometry y="90" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="171" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="170" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="172" value="product_id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="170" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="188" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="160" vertex="1">
|
||||||
|
<mxGeometry y="120" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="189" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="188" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="190" value="brand_id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="188" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="173" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="160" vertex="1">
|
||||||
|
<mxGeometry y="150" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="174" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="173" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="175" value="organic: BOOLEAN" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="173" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="176" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="160" vertex="1">
|
||||||
|
<mxGeometry y="180" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="177" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="176" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="178" value="weight: FLOAT" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="176" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="179" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="160" vertex="1">
|
||||||
|
<mxGeometry y="210" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="180" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="179" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="181" value="weight_unit: FLOAT" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="179" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="182" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="160" vertex="1">
|
||||||
|
<mxGeometry y="240" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="183" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="182" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="184" value="created_at: DATETIME" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="182" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="185" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="160" vertex="1">
|
||||||
|
<mxGeometry y="270" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="186" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="185" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="187" value="updated_at: DATETIME" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="185" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="191" value="<span style="color: rgb(0, 0, 0); text-wrap-mode: wrap;">brands</span>" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="390" y="790" width="180" height="150" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="192" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="191" vertex="1">
|
||||||
|
<mxGeometry y="30" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="193" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="192" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="194" value="id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" parent="192" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="195" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="191" vertex="1">
|
||||||
|
<mxGeometry y="60" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="196" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" parent="195" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="197" value="name: STRING" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="195" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="213" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="191" vertex="1">
|
||||||
|
<mxGeometry y="90" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="214" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="213" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="215" value="created_at: DATETIME" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="213" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="216" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" parent="191" vertex="1">
|
||||||
|
<mxGeometry y="120" width="180" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="217" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;" parent="216" vertex="1">
|
||||||
|
<mxGeometry width="30" height="30" as="geometry">
|
||||||
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="218" value="updated_at: DATETIME" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;" parent="216" vertex="1">
|
||||||
|
<mxGeometry x="30" width="150" height="30" as="geometry">
|
||||||
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="219" value="" style="endArrow=ERmany;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=ERone;startFill=0;endFill=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="192" target="188" edge="1">
|
||||||
|
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||||
|
<mxPoint x="900" y="500" as="sourcePoint"/>
|
||||||
|
<mxPoint x="1100" y="375" as="targetPoint"/>
|
||||||
|
<Array as="points"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="220" value="" style="endArrow=ERmany;html=1;rounded=0;startArrow=ERone;startFill=0;endFill=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="3" target="170" edge="1">
|
||||||
|
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||||
|
<mxPoint x="580" y="845" as="sourcePoint"/>
|
||||||
|
<mxPoint x="670" y="560" as="targetPoint"/>
|
||||||
|
<Array as="points"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="221" value="" style="endArrow=ERmany;html=1;rounded=0;startArrow=ERone;startFill=0;endFill=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="120" target="144" edge="1">
|
||||||
|
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||||
|
<mxPoint x="580" y="495" as="sourcePoint"/>
|
||||||
|
<mxPoint x="720" y="560" as="targetPoint"/>
|
||||||
|
<Array as="points"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<mxfile host="65bd71144e">
|
<mxfile host="65bd71144e">
|
||||||
<diagram name="Grocery Tracker Database Schema" id="database-schema">
|
<diagram name="Product Tracker Database Schema" id="database-schema">
|
||||||
<mxGraphModel dx="547" dy="520" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
|
<mxGraphModel dx="547" dy="520" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
|
||||||
<root>
|
<root>
|
||||||
<mxCell id="0"/>
|
<mxCell id="0"/>
|
||||||
@ -22,7 +22,7 @@
|
|||||||
<mxCell id="event-association-label" value="1:N" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;" parent="1" vertex="1">
|
<mxCell id="event-association-label" value="1:N" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;" parent="1" vertex="1">
|
||||||
<mxGeometry x="650" y="280" width="40" height="30" as="geometry"/>
|
<mxGeometry x="650" y="280" width="40" height="30" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="grocery-association-relation" value="" style="endArrow=ERmany;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=ERone;startFill=0;endFill=0;" parent="1" source="3" target="102" edge="1">
|
<mxCell id="product-association-relation" value="" style="endArrow=ERmany;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=ERone;startFill=0;endFill=0;" parent="1" source="3" target="102" edge="1">
|
||||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||||
<mxPoint x="280" y="150" as="sourcePoint"/>
|
<mxPoint x="280" y="150" as="sourcePoint"/>
|
||||||
<mxPoint x="720" y="290" as="targetPoint"/>
|
<mxPoint x="720" y="290" as="targetPoint"/>
|
||||||
@ -32,7 +32,7 @@
|
|||||||
</Array>
|
</Array>
|
||||||
</mxGeometry>
|
</mxGeometry>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="grocery-association-label" value="1:N" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;" parent="1" vertex="1">
|
<mxCell id="product-association-label" value="1:N" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;" parent="1" vertex="1">
|
||||||
<mxGeometry x="630" y="220" width="40" height="30" as="geometry"/>
|
<mxGeometry x="630" y="220" width="40" height="30" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="legend" value="" style="swimlane;fontStyle=1;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
|
<mxCell id="legend" value="" style="swimlane;fontStyle=1;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
|
||||||
@ -50,10 +50,10 @@
|
|||||||
<mxCell id="legend-relation" value="1:N = One-to-Many Relationship" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" parent="legend" vertex="1">
|
<mxCell id="legend-relation" value="1:N = One-to-Many Relationship" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" parent="legend" vertex="1">
|
||||||
<mxGeometry y="90" width="300" height="20" as="geometry"/>
|
<mxGeometry y="90" width="300" height="20" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="diagram-title" value="Grocery Tracker Database Schema" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=20;fontStyle=1;" parent="1" vertex="1">
|
<mxCell id="diagram-title" value="Product Tracker Database Schema" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=20;fontStyle=1;" parent="1" vertex="1">
|
||||||
<mxGeometry x="400" y="20" width="320" height="40" as="geometry"/>
|
<mxGeometry x="400" y="20" width="320" height="40" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="2" value="<span style="color: rgb(0, 0, 0); text-wrap-mode: wrap;">groceries</span>" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" parent="1" vertex="1">
|
<mxCell id="2" value="<span style="color: rgb(0, 0, 0); text-wrap-mode: wrap;">products</span>" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" parent="1" vertex="1">
|
||||||
<mxGeometry x="70" y="70" width="180" height="270" as="geometry"/>
|
<mxGeometry x="70" y="70" width="180" height="270" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="3" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="2" vertex="1">
|
<mxCell id="3" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="2" vertex="1">
|
||||||
@ -335,7 +335,7 @@
|
|||||||
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
<mxRectangle width="150" height="30" as="alternateBounds"/>
|
||||||
</mxGeometry>
|
</mxGeometry>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="95" value="<span style="color: rgb(0, 0, 0); text-wrap-mode: wrap;">shopping_event_groceries</span>" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" parent="1" vertex="1">
|
<mxCell id="95" value="<span style="color: rgb(0, 0, 0); text-wrap-mode: wrap;">shopping_event_products</span>" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" parent="1" vertex="1">
|
||||||
<mxGeometry x="810" y="200" width="240" height="180" as="geometry"/>
|
<mxGeometry x="810" y="200" width="240" height="180" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="96" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="95" vertex="1">
|
<mxCell id="96" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" parent="95" vertex="1">
|
||||||
@ -372,7 +372,7 @@
|
|||||||
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
<mxRectangle width="30" height="30" as="alternateBounds"/>
|
||||||
</mxGeometry>
|
</mxGeometry>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="104" value="grocery_id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="102" vertex="1">
|
<mxCell id="104" value="product_id: INTEGER" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" parent="102" vertex="1">
|
||||||
<mxGeometry x="30" width="210" height="30" as="geometry">
|
<mxGeometry x="30" width="210" height="30" as="geometry">
|
||||||
<mxRectangle width="210" height="30" as="alternateBounds"/>
|
<mxRectangle width="210" height="30" as="alternateBounds"/>
|
||||||
</mxGeometry>
|
</mxGeometry>
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "grocery-tracker-frontend",
|
"name": "product-tracker-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "grocery-tracker-frontend",
|
"name": "product-tracker-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^20.10.5",
|
"@types/node": "^20.10.5",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "grocery-tracker-frontend",
|
"name": "product-tracker-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Track grocery prices and shopping events"
|
content="Track product prices and shopping events"
|
||||||
/>
|
/>
|
||||||
<title>Grocery Tracker</title>
|
<title>Product Tracker</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
echo "🎯 Setting up Grocery Tracker Frontend"
|
echo "🎯 Setting up Product Tracker Frontend"
|
||||||
echo "======================================"
|
echo "======================================"
|
||||||
|
|
||||||
# Check if Node.js is installed
|
# Check if Node.js is installed
|
||||||
|
|||||||
@ -1,67 +1,64 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Link, useLocation } 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 Dashboard from './components/Dashboard';
|
import Dashboard from './components/Dashboard';
|
||||||
|
import ProductList from './components/ProductList';
|
||||||
|
import ShopList from './components/ShopList';
|
||||||
|
import ShoppingEventList from './components/ShoppingEventList';
|
||||||
|
import ShoppingEventForm from './components/ShoppingEventForm';
|
||||||
|
|
||||||
|
function Navigation() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isActive = (path: string) => {
|
||||||
|
return location.pathname === path;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="bg-blue-600 text-white p-4">
|
||||||
|
<div className="container mx-auto flex justify-between items-center">
|
||||||
|
<Link to="/" className="text-xl font-bold">
|
||||||
|
Product Tracker
|
||||||
|
</Link>
|
||||||
|
<div className="space-x-4">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className={`px-3 py-2 rounded ${isActive('/') ? 'bg-blue-800' : 'hover:bg-blue-700'}`}
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/products"
|
||||||
|
className={`px-3 py-2 rounded ${isActive('/products') ? 'bg-blue-800' : 'hover:bg-blue-700'}`}
|
||||||
|
>
|
||||||
|
Products
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/shops"
|
||||||
|
className={`px-3 py-2 rounded ${isActive('/shops') ? 'bg-blue-800' : 'hover:bg-blue-700'}`}
|
||||||
|
>
|
||||||
|
Shops
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/shopping-events"
|
||||||
|
className={`px-3 py-2 rounded ${isActive('/shopping-events') ? 'bg-blue-800' : 'hover:bg-blue-700'}`}
|
||||||
|
>
|
||||||
|
Shopping Events
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-100">
|
||||||
{/* Navigation */}
|
<Navigation />
|
||||||
<nav className="bg-white shadow-lg">
|
<main className="container mx-auto py-8 px-4">
|
||||||
<div className="max-w-7xl mx-auto px-4">
|
|
||||||
<div className="flex justify-between h-16">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex-shrink-0 flex items-center">
|
|
||||||
<h1 className="text-xl font-bold text-gray-800">
|
|
||||||
🛒 Grocery Tracker
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className="text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium"
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/groceries"
|
|
||||||
className="text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium"
|
|
||||||
>
|
|
||||||
Groceries
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/shops"
|
|
||||||
className="text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium"
|
|
||||||
>
|
|
||||||
Shops
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/shopping-events"
|
|
||||||
className="text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium"
|
|
||||||
>
|
|
||||||
Shopping Events
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/add-purchase"
|
|
||||||
className="bg-blue-500 hover:bg-blue-700 text-white inline-flex items-center px-3 py-2 text-sm font-medium rounded-md"
|
|
||||||
>
|
|
||||||
Add New Event
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route path="/groceries" element={<GroceryList />} />
|
<Route path="/products" element={<ProductList />} />
|
||||||
<Route path="/shops" element={<ShopList />} />
|
<Route path="/shops" element={<ShopList />} />
|
||||||
<Route path="/shopping-events" element={<ShoppingEventList />} />
|
<Route path="/shopping-events" element={<ShoppingEventList />} />
|
||||||
<Route path="/shopping-events/:id/edit" element={<ShoppingEventForm />} />
|
<Route path="/shopping-events/:id/edit" element={<ShoppingEventForm />} />
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { groceryApi } from '../services/api';
|
import { productApi } from '../services/api';
|
||||||
import { Grocery } from '../types';
|
import { Product } from '../types';
|
||||||
|
|
||||||
interface AddGroceryModalProps {
|
interface AddProductModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onGroceryAdded: () => void;
|
onProductAdded: () => void;
|
||||||
editGrocery?: Grocery | null;
|
editProduct?: Product | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GroceryFormData {
|
interface ProductFormData {
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
category: string;
|
||||||
organic: boolean;
|
organic: boolean;
|
||||||
@ -17,8 +17,8 @@ interface GroceryFormData {
|
|||||||
weight_unit: string;
|
weight_unit: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGroceryAdded, editGrocery }) => {
|
const AddProductModal: React.FC<AddProductModalProps> = ({ isOpen, onClose, onProductAdded, editProduct }) => {
|
||||||
const [formData, setFormData] = useState<GroceryFormData>({
|
const [formData, setFormData] = useState<ProductFormData>({
|
||||||
name: '',
|
name: '',
|
||||||
category: '',
|
category: '',
|
||||||
organic: false,
|
organic: false,
|
||||||
@ -37,16 +37,16 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
|
|||||||
|
|
||||||
// Populate form when editing
|
// Populate form when editing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editGrocery) {
|
if (editProduct) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: editGrocery.name,
|
name: editProduct.name,
|
||||||
category: editGrocery.category,
|
category: editProduct.category,
|
||||||
organic: editGrocery.organic,
|
organic: editProduct.organic,
|
||||||
weight: editGrocery.weight,
|
weight: editProduct.weight,
|
||||||
weight_unit: editGrocery.weight_unit
|
weight_unit: editProduct.weight_unit
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Reset form for adding new grocery
|
// Reset form for adding new product
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
category: '',
|
category: '',
|
||||||
@ -56,7 +56,7 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setError('');
|
setError('');
|
||||||
}, [editGrocery, isOpen]);
|
}, [editProduct, isOpen]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -69,17 +69,17 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
const groceryData = {
|
const productData = {
|
||||||
...formData,
|
...formData,
|
||||||
weight: formData.weight || undefined
|
weight: formData.weight || undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
if (editGrocery) {
|
if (editProduct) {
|
||||||
// Update existing grocery
|
// Update existing product
|
||||||
await groceryApi.update(editGrocery.id, groceryData);
|
await productApi.update(editProduct.id, productData);
|
||||||
} else {
|
} else {
|
||||||
// Create new grocery
|
// Create new product
|
||||||
await groceryApi.create(groceryData);
|
await productApi.create(productData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
@ -91,11 +91,11 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
|
|||||||
weight_unit: 'piece'
|
weight_unit: 'piece'
|
||||||
});
|
});
|
||||||
|
|
||||||
onGroceryAdded();
|
onProductAdded();
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`Failed to ${editGrocery ? 'update' : 'add'} grocery. Please try again.`);
|
setError(`Failed to ${editProduct ? 'update' : 'add'} product. Please try again.`);
|
||||||
console.error(`Error ${editGrocery ? 'updating' : 'adding'} grocery:`, err);
|
console.error(`Error ${editProduct ? 'updating' : 'adding'} product:`, err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -119,7 +119,7 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
|
|||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="text-lg font-medium text-gray-900">
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
{editGrocery ? 'Edit Grocery' : 'Add New Grocery'}
|
{editProduct ? 'Edit Product' : 'Add New Product'}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@ -236,8 +236,8 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
|
|||||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{loading
|
{loading
|
||||||
? (editGrocery ? 'Updating...' : 'Adding...')
|
? (editProduct ? 'Updating...' : 'Adding...')
|
||||||
: (editGrocery ? 'Update Grocery' : 'Add Grocery')
|
: (editProduct ? 'Update Product' : 'Add Product')
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -248,4 +248,4 @@ const AddGroceryModal: React.FC<AddGroceryModalProps> = ({ isOpen, onClose, onGr
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddGroceryModal;
|
export default AddProductModal;
|
||||||
@ -32,7 +32,7 @@ const Dashboard: React.FC = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
||||||
<p className="text-gray-600">Welcome to your grocery tracker!</p>
|
<p className="text-gray-600">Welcome to your product tracker!</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
@ -102,7 +102,7 @@ const Dashboard: React.FC = () => {
|
|||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/add-purchase')}
|
onClick={() => navigate('/shopping-events')}
|
||||||
className="flex items-center p-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
className="flex items-center p-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="p-2 bg-blue-100 rounded-md mr-3">
|
<div className="p-2 bg-blue-100 rounded-md mr-3">
|
||||||
@ -117,7 +117,7 @@ const Dashboard: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/groceries?add=true')}
|
onClick={() => navigate('/products?add=true')}
|
||||||
className="flex items-center p-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
className="flex items-center p-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="p-2 bg-green-100 rounded-md mr-3">
|
<div className="p-2 bg-green-100 rounded-md mr-3">
|
||||||
@ -126,8 +126,8 @@ const Dashboard: React.FC = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">Add Grocery</p>
|
<p className="font-medium text-gray-900">Add Product</p>
|
||||||
<p className="text-sm text-gray-600">Add a new grocery item</p>
|
<p className="text-sm text-gray-600">Add a new product item</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -181,9 +181,9 @@ const Dashboard: React.FC = () => {
|
|||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
{new Date(event.date).toLocaleDateString()}
|
{new Date(event.date).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
{event.groceries.length > 0 && (
|
{event.products.length > 0 && (
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
{event.groceries.length} item{event.groceries.length !== 1 ? 's' : ''}
|
{event.products.length} item{event.products.length !== 1 ? 's' : ''}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,22 +1,22 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { Grocery } from '../types';
|
import { Product } from '../types';
|
||||||
import { groceryApi } from '../services/api';
|
import { productApi } from '../services/api';
|
||||||
import AddGroceryModal from './AddGroceryModal';
|
import AddProductModal from './AddProductModal';
|
||||||
import ConfirmDeleteModal from './ConfirmDeleteModal';
|
import ConfirmDeleteModal from './ConfirmDeleteModal';
|
||||||
|
|
||||||
const GroceryList: React.FC = () => {
|
const ProductList: React.FC = () => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [groceries, setGroceries] = useState<Grocery[]>([]);
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [editingGrocery, setEditingGrocery] = useState<Grocery | null>(null);
|
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
|
||||||
const [deletingGrocery, setDeletingGrocery] = useState<Grocery | null>(null);
|
const [deletingProduct, setDeletingProduct] = useState<Product | null>(null);
|
||||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchGroceries();
|
fetchProducts();
|
||||||
|
|
||||||
// Check if we should auto-open the modal
|
// Check if we should auto-open the modal
|
||||||
if (searchParams.get('add') === 'true') {
|
if (searchParams.get('add') === 'true') {
|
||||||
@ -26,55 +26,55 @@ const GroceryList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [searchParams, setSearchParams]);
|
}, [searchParams, setSearchParams]);
|
||||||
|
|
||||||
const fetchGroceries = async () => {
|
const fetchProducts = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await groceryApi.getAll();
|
const response = await productApi.getAll();
|
||||||
setGroceries(response.data);
|
setProducts(response.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Failed to fetch groceries');
|
setError('Failed to fetch products');
|
||||||
console.error('Error fetching groceries:', err);
|
console.error('Error fetching products:', err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (grocery: Grocery) => {
|
const handleEdit = (product: Product) => {
|
||||||
setEditingGrocery(grocery);
|
setEditingProduct(product);
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (grocery: Grocery) => {
|
const handleDelete = (product: Product) => {
|
||||||
setDeletingGrocery(grocery);
|
setDeletingProduct(product);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
if (!deletingGrocery) return;
|
if (!deletingProduct) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setDeleteLoading(true);
|
setDeleteLoading(true);
|
||||||
await groceryApi.delete(deletingGrocery.id);
|
await productApi.delete(deletingProduct.id);
|
||||||
setDeletingGrocery(null);
|
setDeletingProduct(null);
|
||||||
fetchGroceries(); // Refresh the list
|
fetchProducts(); // Refresh the list
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error deleting grocery:', err);
|
console.error('Error deleting product:', err);
|
||||||
setError('Failed to delete grocery. Please try again.');
|
setError('Failed to delete product. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setDeleteLoading(false);
|
setDeleteLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGroceryAdded = () => {
|
const handleProductAdded = () => {
|
||||||
fetchGroceries(); // Refresh the list
|
fetchProducts(); // Refresh the list
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
setEditingGrocery(null);
|
setEditingProduct(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseDeleteModal = () => {
|
const handleCloseDeleteModal = () => {
|
||||||
setDeletingGrocery(null);
|
setDeletingProduct(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -88,15 +88,15 @@ const GroceryList: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Groceries</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Products</h1>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingGrocery(null);
|
setEditingProduct(null);
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
}}
|
}}
|
||||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||||
>
|
>
|
||||||
Add New Grocery
|
Add New Product
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -107,13 +107,13 @@ const GroceryList: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="bg-white shadow rounded-lg overflow-hidden">
|
<div className="bg-white shadow rounded-lg overflow-hidden">
|
||||||
{groceries.length === 0 ? (
|
{products.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 48 48">
|
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 48 48">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||||
</svg>
|
</svg>
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900">No groceries</h3>
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No products</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500">Get started by adding your first grocery item.</p>
|
<p className="mt-1 text-sm text-gray-500">Get started by adding your first product item.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
@ -137,39 +137,39 @@ const GroceryList: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{groceries.map((grocery) => (
|
{products.map((product) => (
|
||||||
<tr key={grocery.id} className="hover:bg-gray-50">
|
<tr key={product.id} className="hover:bg-gray-50">
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm font-medium text-gray-900">
|
<div className="text-sm font-medium text-gray-900">
|
||||||
{grocery.name} {grocery.organic ? '🌱' : ''}
|
{product.name} {product.organic ? '🌱' : ''}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
|
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
|
||||||
{grocery.category}
|
{product.category}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
{grocery.weight ? `${grocery.weight}${grocery.weight_unit}` : '-'}
|
{product.weight ? `${product.weight}${product.weight_unit}` : '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
||||||
grocery.organic
|
product.organic
|
||||||
? 'bg-green-100 text-green-800'
|
? 'bg-green-100 text-green-800'
|
||||||
: 'bg-gray-100 text-gray-800'
|
: 'bg-gray-100 text-gray-800'
|
||||||
}`}>
|
}`}>
|
||||||
{grocery.organic ? 'Organic' : 'Conventional'}
|
{product.organic ? 'Organic' : 'Conventional'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEdit(grocery)}
|
onClick={() => handleEdit(product)}
|
||||||
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(grocery)}
|
onClick={() => handleDelete(product)}
|
||||||
className="text-red-600 hover:text-red-900"
|
className="text-red-600 hover:text-red-900"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
@ -182,23 +182,23 @@ const GroceryList: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddGroceryModal
|
<AddProductModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={handleCloseModal}
|
onClose={handleCloseModal}
|
||||||
onGroceryAdded={handleGroceryAdded}
|
onProductAdded={handleProductAdded}
|
||||||
editGrocery={editingGrocery}
|
editProduct={editingProduct}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmDeleteModal
|
<ConfirmDeleteModal
|
||||||
isOpen={!!deletingGrocery}
|
isOpen={!!deletingProduct}
|
||||||
onClose={handleCloseDeleteModal}
|
onClose={handleCloseDeleteModal}
|
||||||
onConfirm={confirmDelete}
|
onConfirm={confirmDelete}
|
||||||
title="Delete Grocery"
|
title="Delete Product"
|
||||||
message={`Are you sure you want to delete "${deletingGrocery?.name}"? This action cannot be undone.`}
|
message={`Are you sure you want to delete "${deletingProduct?.name}"? This action cannot be undone.`}
|
||||||
isLoading={deleteLoading}
|
isLoading={deleteLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GroceryList;
|
export default ProductList;
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Shop, Grocery, ShoppingEventCreate, GroceryInEvent } from '../types';
|
import { Shop, Product, ShoppingEventCreate, ProductInEvent } from '../types';
|
||||||
import { shopApi, groceryApi, shoppingEventApi } from '../services/api';
|
import { shopApi, productApi, shoppingEventApi } from '../services/api';
|
||||||
|
|
||||||
const ShoppingEventForm: React.FC = () => {
|
const ShoppingEventForm: React.FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [shops, setShops] = useState<Shop[]>([]);
|
const [shops, setShops] = useState<Shop[]>([]);
|
||||||
const [groceries, setGroceries] = useState<Grocery[]>([]);
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadingEvent, setLoadingEvent] = useState(false);
|
const [loadingEvent, setLoadingEvent] = useState(false);
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
@ -19,12 +19,12 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
date: new Date().toISOString().split('T')[0],
|
date: new Date().toISOString().split('T')[0],
|
||||||
total_amount: undefined,
|
total_amount: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
groceries: []
|
products: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedGroceries, setSelectedGroceries] = useState<GroceryInEvent[]>([]);
|
const [selectedProducts, setSelectedProducts] = useState<ProductInEvent[]>([]);
|
||||||
const [newGroceryItem, setNewGroceryItem] = useState<GroceryInEvent>({
|
const [newProductItem, setNewProductItem] = useState<ProductInEvent>({
|
||||||
grocery_id: 0,
|
product_id: 0,
|
||||||
amount: 1,
|
amount: 1,
|
||||||
price: 0
|
price: 0
|
||||||
});
|
});
|
||||||
@ -32,28 +32,28 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchShops();
|
fetchShops();
|
||||||
fetchGroceries();
|
fetchProducts();
|
||||||
if (isEditMode && id) {
|
if (isEditMode && id) {
|
||||||
fetchShoppingEvent(parseInt(id));
|
fetchShoppingEvent(parseInt(id));
|
||||||
}
|
}
|
||||||
}, [id, isEditMode]);
|
}, [id, isEditMode]);
|
||||||
|
|
||||||
// Calculate total amount from selected groceries
|
// Calculate total amount from selected products
|
||||||
const calculateTotal = (groceries: GroceryInEvent[]): number => {
|
const calculateTotal = (products: ProductInEvent[]): number => {
|
||||||
const total = groceries.reduce((total, item) => total + (item.amount * item.price), 0);
|
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
|
return Math.round(total * 100) / 100; // Round to 2 decimal places to avoid floating-point errors
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update total amount whenever selectedGroceries changes
|
// Update total amount whenever selectedProducts changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoCalculate) {
|
if (autoCalculate) {
|
||||||
const calculatedTotal = calculateTotal(selectedGroceries);
|
const calculatedTotal = calculateTotal(selectedProducts);
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
total_amount: calculatedTotal > 0 ? calculatedTotal : undefined
|
total_amount: calculatedTotal > 0 ? calculatedTotal : undefined
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [selectedGroceries, autoCalculate]);
|
}, [selectedProducts, autoCalculate]);
|
||||||
|
|
||||||
const fetchShops = async () => {
|
const fetchShops = async () => {
|
||||||
try {
|
try {
|
||||||
@ -64,12 +64,12 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchGroceries = async () => {
|
const fetchProducts = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await groceryApi.getAll();
|
const response = await productApi.getAll();
|
||||||
setGroceries(response.data);
|
setProducts(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching groceries:', error);
|
console.error('Error fetching products:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,15 +86,15 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
formattedDate = event.date.split('T')[0];
|
formattedDate = event.date.split('T')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map groceries to the format we need
|
// Map products to the format we need
|
||||||
const mappedGroceries = event.groceries.map(g => ({
|
const mappedProducts = event.products.map(p => ({
|
||||||
grocery_id: g.id,
|
product_id: p.id,
|
||||||
amount: g.amount,
|
amount: p.amount,
|
||||||
price: g.price
|
price: p.price
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Calculate the sum of all groceries
|
// Calculate the sum of all products
|
||||||
const calculatedTotal = calculateTotal(mappedGroceries);
|
const calculatedTotal = calculateTotal(mappedProducts);
|
||||||
|
|
||||||
// Check if existing total matches calculated total (with small tolerance for floating point)
|
// Check if existing total matches calculated total (with small tolerance for floating point)
|
||||||
const existingTotal = event.total_amount || 0;
|
const existingTotal = event.total_amount || 0;
|
||||||
@ -105,10 +105,10 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
date: formattedDate,
|
date: formattedDate,
|
||||||
total_amount: event.total_amount,
|
total_amount: event.total_amount,
|
||||||
notes: event.notes || '',
|
notes: event.notes || '',
|
||||||
groceries: []
|
products: []
|
||||||
});
|
});
|
||||||
|
|
||||||
setSelectedGroceries(mappedGroceries);
|
setSelectedProducts(mappedProducts);
|
||||||
setAutoCalculate(totalMatches); // Enable auto-calc if totals match, disable if they don't
|
setAutoCalculate(totalMatches); // Enable auto-calc if totals match, disable if they don't
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching shopping event:', error);
|
console.error('Error fetching shopping event:', error);
|
||||||
@ -118,27 +118,27 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addGroceryToEvent = () => {
|
const addProductToEvent = () => {
|
||||||
if (newGroceryItem.grocery_id > 0 && newGroceryItem.amount > 0 && newGroceryItem.price >= 0) {
|
if (newProductItem.product_id > 0 && newProductItem.amount > 0 && newProductItem.price >= 0) {
|
||||||
setSelectedGroceries([...selectedGroceries, { ...newGroceryItem }]);
|
setSelectedProducts([...selectedProducts, { ...newProductItem }]);
|
||||||
setNewGroceryItem({ grocery_id: 0, amount: 1, price: 0 });
|
setNewProductItem({ product_id: 0, amount: 1, price: 0 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeGroceryFromEvent = (index: number) => {
|
const removeProductFromEvent = (index: number) => {
|
||||||
setSelectedGroceries(selectedGroceries.filter((_, i) => i !== index));
|
setSelectedProducts(selectedProducts.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
const editGroceryFromEvent = (index: number) => {
|
const editProductFromEvent = (index: number) => {
|
||||||
const groceryToEdit = selectedGroceries[index];
|
const productToEdit = selectedProducts[index];
|
||||||
// Load the grocery data into the input fields
|
// Load the product data into the input fields
|
||||||
setNewGroceryItem({
|
setNewProductItem({
|
||||||
grocery_id: groceryToEdit.grocery_id,
|
product_id: productToEdit.product_id,
|
||||||
amount: groceryToEdit.amount,
|
amount: productToEdit.amount,
|
||||||
price: groceryToEdit.price
|
price: productToEdit.price
|
||||||
});
|
});
|
||||||
// Remove the item from the selected list
|
// Remove the item from the selected list
|
||||||
setSelectedGroceries(selectedGroceries.filter((_, i) => i !== index));
|
setSelectedProducts(selectedProducts.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
@ -149,7 +149,7 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const eventData = {
|
const eventData = {
|
||||||
...formData,
|
...formData,
|
||||||
groceries: selectedGroceries
|
products: selectedProducts
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
@ -173,9 +173,9 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
date: new Date().toISOString().split('T')[0],
|
date: new Date().toISOString().split('T')[0],
|
||||||
total_amount: undefined,
|
total_amount: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
groceries: []
|
products: []
|
||||||
});
|
});
|
||||||
setSelectedGroceries([]);
|
setSelectedProducts([]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Full error object:', error);
|
console.error('Full error object:', error);
|
||||||
@ -185,13 +185,13 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGroceryName = (id: number) => {
|
const getProductName = (id: number) => {
|
||||||
const grocery = groceries.find(g => g.id === id);
|
const product = products.find(p => p.id === id);
|
||||||
if (!grocery) return 'Unknown';
|
if (!product) return 'Unknown';
|
||||||
|
|
||||||
const weightInfo = grocery.weight ? `${grocery.weight}${grocery.weight_unit}` : grocery.weight_unit;
|
const weightInfo = product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit;
|
||||||
const organicEmoji = grocery.organic ? ' 🌱' : '';
|
const organicEmoji = product.organic ? ' 🌱' : '';
|
||||||
return `${grocery.name}${organicEmoji} ${weightInfo}`;
|
return `${product.name}${organicEmoji} ${weightInfo}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loadingEvent) {
|
if (loadingEvent) {
|
||||||
@ -265,25 +265,25 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add Groceries Section */}
|
{/* Add Products Section */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Add Groceries
|
Add Products
|
||||||
</label>
|
</label>
|
||||||
<div className="flex space-x-2 mb-4">
|
<div className="flex space-x-2 mb-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||||
Grocery
|
Product
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={newGroceryItem.grocery_id}
|
value={newProductItem.product_id}
|
||||||
onChange={(e) => setNewGroceryItem({...newGroceryItem, grocery_id: parseInt(e.target.value)})}
|
onChange={(e) => setNewProductItem({...newProductItem, product_id: parseInt(e.target.value)})}
|
||||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value={0}>Select a grocery</option>
|
<option value={0}>Select a product</option>
|
||||||
{groceries.map(grocery => (
|
{products.map(product => (
|
||||||
<option key={grocery.id} value={grocery.id}>
|
<option key={product.id} value={product.id}>
|
||||||
{grocery.name}{grocery.organic ? '🌱' : ''} ({grocery.category}) {grocery.weight ? `${grocery.weight}${grocery.weight_unit}` : grocery.weight_unit}
|
{product.name}{product.organic ? '🌱' : ''} ({product.category}) {product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@ -297,8 +297,8 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
step="1"
|
step="1"
|
||||||
min="1"
|
min="1"
|
||||||
placeholder="1"
|
placeholder="1"
|
||||||
value={newGroceryItem.amount}
|
value={newProductItem.amount}
|
||||||
onChange={(e) => setNewGroceryItem({...newGroceryItem, amount: parseFloat(e.target.value)})}
|
onChange={(e) => setNewProductItem({...newProductItem, amount: parseFloat(e.target.value)})}
|
||||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -311,15 +311,15 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
min="0"
|
min="0"
|
||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
value={newGroceryItem.price}
|
value={newProductItem.price}
|
||||||
onChange={(e) => setNewGroceryItem({...newGroceryItem, price: parseFloat(e.target.value)})}
|
onChange={(e) => setNewProductItem({...newProductItem, price: parseFloat(e.target.value)})}
|
||||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={addGroceryToEvent}
|
onClick={addProductToEvent}
|
||||||
className="bg-green-500 hover:bg-green-700 text-white px-4 py-2 rounded-md"
|
className="bg-green-500 hover:bg-green-700 text-white px-4 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
@ -327,15 +327,15 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Selected Groceries List */}
|
{/* Selected Products List */}
|
||||||
{selectedGroceries.length > 0 && (
|
{selectedProducts.length > 0 && (
|
||||||
<div className="bg-gray-50 rounded-md p-4">
|
<div className="bg-gray-50 rounded-md p-4">
|
||||||
<h4 className="font-medium text-gray-700 mb-2">Selected Items:</h4>
|
<h4 className="font-medium text-gray-700 mb-2">Selected Items:</h4>
|
||||||
{selectedGroceries.map((item, index) => (
|
{selectedProducts.map((item, index) => (
|
||||||
<div key={index} className="flex justify-between items-center py-2 border-b last:border-b-0">
|
<div key={index} className="flex justify-between items-center py-2 border-b last:border-b-0">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-sm text-gray-900">
|
<div className="text-sm text-gray-900">
|
||||||
{getGroceryName(item.grocery_id)}
|
{getProductName(item.product_id)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-600">
|
<div className="text-xs text-gray-600">
|
||||||
{item.amount} × ${item.price.toFixed(2)} = ${(item.amount * item.price).toFixed(2)}
|
{item.amount} × ${item.price.toFixed(2)} = ${(item.amount * item.price).toFixed(2)}
|
||||||
@ -344,14 +344,14 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => editGroceryFromEvent(index)}
|
onClick={() => editProductFromEvent(index)}
|
||||||
className="text-blue-500 hover:text-blue-700"
|
className="text-blue-500 hover:text-blue-700"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeGroceryFromEvent(index)}
|
onClick={() => removeProductFromEvent(index)}
|
||||||
className="text-red-500 hover:text-red-700"
|
className="text-red-500 hover:text-red-700"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
@ -431,7 +431,7 @@ const ShoppingEventForm: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || formData.shop_id === 0 || selectedGroceries.length === 0}
|
disabled={loading || formData.shop_id === 0 || selectedProducts.length === 0}
|
||||||
className={`px-4 py-2 text-sm font-medium text-white rounded-md disabled:opacity-50 disabled:cursor-not-allowed ${
|
className={`px-4 py-2 text-sm font-medium text-white rounded-md disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||||
isEditMode
|
isEditMode
|
||||||
? 'bg-blue-600 hover:bg-blue-700'
|
? 'bg-blue-600 hover:bg-blue-700'
|
||||||
|
|||||||
@ -109,17 +109,17 @@ const ShoppingEventList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{event.groceries.length > 0 && (
|
{event.products.length > 0 && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h4 className="text-sm font-medium text-gray-700 mb-2">Items Purchased:</h4>
|
<h4 className="text-sm font-medium text-gray-700 mb-2">Items Purchased:</h4>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||||
{event.groceries.map((grocery) => (
|
{event.products.map((product) => (
|
||||||
<div key={grocery.id} className="bg-gray-50 rounded px-3 py-2">
|
<div key={product.id} className="bg-gray-50 rounded px-3 py-2">
|
||||||
<div className="text-sm text-gray-900">
|
<div className="text-sm text-gray-900">
|
||||||
{grocery.name} {grocery.organic ? '🌱' : ''}
|
{product.name} {product.organic ? '🌱' : ''}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-600">
|
<div className="text-xs text-gray-600">
|
||||||
{grocery.amount} × ${grocery.price.toFixed(2)} = ${(grocery.amount * grocery.price).toFixed(2)}
|
{product.amount} × ${product.price.toFixed(2)} = ${(product.amount * product.price).toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,23 +1,34 @@
|
|||||||
import axios from 'axios';
|
import { Product, ProductCreate, Shop, ShopCreate, ShoppingEvent, ShoppingEventCreate } from '../types';
|
||||||
import { Grocery, GroceryCreate, Shop, ShopCreate, ShoppingEvent, ShoppingEventCreate } from '../types';
|
|
||||||
|
|
||||||
const BASE_URL = 'http://localhost:8000';
|
const API_BASE_URL = 'http://localhost:8000';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = {
|
||||||
baseURL: BASE_URL,
|
get: <T>(url: string): Promise<{ data: T }> =>
|
||||||
headers: {
|
fetch(`${API_BASE_URL}${url}`).then(res => res.json()).then(data => ({ data })),
|
||||||
'Content-Type': 'application/json',
|
post: <T>(url: string, body: any): Promise<{ data: T }> =>
|
||||||
},
|
fetch(`${API_BASE_URL}${url}`, {
|
||||||
});
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}).then(res => res.json()).then(data => ({ data })),
|
||||||
|
put: <T>(url: string, body: any): Promise<{ data: T }> =>
|
||||||
|
fetch(`${API_BASE_URL}${url}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}).then(res => res.json()).then(data => ({ data })),
|
||||||
|
delete: (url: string): Promise<void> =>
|
||||||
|
fetch(`${API_BASE_URL}${url}`, { method: 'DELETE' }).then(() => {}),
|
||||||
|
};
|
||||||
|
|
||||||
// Grocery API functions
|
// Product API functions
|
||||||
export const groceryApi = {
|
export const productApi = {
|
||||||
getAll: () => api.get<Grocery[]>('/groceries/'),
|
getAll: () => api.get<Product[]>('/products/'),
|
||||||
getById: (id: number) => api.get<Grocery>(`/groceries/${id}`),
|
getById: (id: number) => api.get<Product>(`/products/${id}`),
|
||||||
create: (grocery: GroceryCreate) => api.post<Grocery>('/groceries/', grocery),
|
create: (product: ProductCreate) => api.post<Product>('/products/', product),
|
||||||
update: (id: number, grocery: Partial<GroceryCreate>) =>
|
update: (id: number, product: Partial<ProductCreate>) =>
|
||||||
api.put<Grocery>(`/groceries/${id}`, grocery),
|
api.put<Product>(`/products/${id}`, product),
|
||||||
delete: (id: number) => api.delete(`/groceries/${id}`),
|
delete: (id: number) => api.delete(`/products/${id}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Shop API functions
|
// Shop API functions
|
||||||
@ -25,7 +36,7 @@ export const shopApi = {
|
|||||||
getAll: () => api.get<Shop[]>('/shops/'),
|
getAll: () => api.get<Shop[]>('/shops/'),
|
||||||
getById: (id: number) => api.get<Shop>(`/shops/${id}`),
|
getById: (id: number) => api.get<Shop>(`/shops/${id}`),
|
||||||
create: (shop: ShopCreate) => api.post<Shop>('/shops/', shop),
|
create: (shop: ShopCreate) => api.post<Shop>('/shops/', shop),
|
||||||
update: (id: number, shop: Partial<ShopCreate>) =>
|
update: (id: number, shop: Partial<ShopCreate>) =>
|
||||||
api.put<Shop>(`/shops/${id}`, shop),
|
api.put<Shop>(`/shops/${id}`, shop),
|
||||||
delete: (id: number) => api.delete(`/shops/${id}`),
|
delete: (id: number) => api.delete(`/shops/${id}`),
|
||||||
};
|
};
|
||||||
@ -34,9 +45,8 @@ export const shopApi = {
|
|||||||
export const shoppingEventApi = {
|
export const shoppingEventApi = {
|
||||||
getAll: () => api.get<ShoppingEvent[]>('/shopping-events/'),
|
getAll: () => api.get<ShoppingEvent[]>('/shopping-events/'),
|
||||||
getById: (id: number) => api.get<ShoppingEvent>(`/shopping-events/${id}`),
|
getById: (id: number) => api.get<ShoppingEvent>(`/shopping-events/${id}`),
|
||||||
create: (event: ShoppingEventCreate) =>
|
create: (event: ShoppingEventCreate) => api.post<ShoppingEvent>('/shopping-events/', event),
|
||||||
api.post<ShoppingEvent>('/shopping-events/', event),
|
update: (id: number, event: ShoppingEventCreate) =>
|
||||||
update: (id: number, event: ShoppingEventCreate) =>
|
|
||||||
api.put<ShoppingEvent>(`/shopping-events/${id}`, event),
|
api.put<ShoppingEvent>(`/shopping-events/${id}`, event),
|
||||||
delete: (id: number) => api.delete(`/shopping-events/${id}`),
|
delete: (id: number) => api.delete(`/shopping-events/${id}`),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export interface Grocery {
|
export interface Product {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
category: string;
|
||||||
@ -9,7 +9,7 @@ export interface Grocery {
|
|||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroceryCreate {
|
export interface ProductCreate {
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
category: string;
|
||||||
organic: boolean;
|
organic: boolean;
|
||||||
@ -32,13 +32,13 @@ export interface ShopCreate {
|
|||||||
address?: string | null;
|
address?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroceryInEvent {
|
export interface ProductInEvent {
|
||||||
grocery_id: number;
|
product_id: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
price: number;
|
price: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroceryWithEventData {
|
export interface ProductWithEventData {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
category: string;
|
||||||
@ -58,7 +58,7 @@ export interface ShoppingEvent {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
shop: Shop;
|
shop: Shop;
|
||||||
groceries: GroceryWithEventData[];
|
products: ProductWithEventData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShoppingEventCreate {
|
export interface ShoppingEventCreate {
|
||||||
@ -66,7 +66,7 @@ export interface ShoppingEventCreate {
|
|||||||
date?: string;
|
date?: string;
|
||||||
total_amount?: number;
|
total_amount?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
groceries: GroceryInEvent[];
|
products: ProductInEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CategoryStats {
|
export interface CategoryStats {
|
||||||
|
|||||||
20
package.json
20
package.json
@ -1,15 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "grocery-tracker-frontend",
|
"name": "product-tracker-frontend",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"description": "React frontend for grocery price tracking application",
|
"description": "React frontend for product price tracking application",
|
||||||
"private": true,
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cd frontend && npm run dev",
|
"dev": "cd frontend && npm run dev",
|
||||||
"build": "cd frontend && npm run build",
|
"build": "cd frontend && npm run build",
|
||||||
"install:frontend": "cd frontend && npm install",
|
"install:frontend": "cd frontend && npm install",
|
||||||
"setup": "npm run install:frontend"
|
"setup": "npm run install:frontend",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"keywords": ["grocery", "price-tracking", "shopping", "react", "fastapi", "python"],
|
"keywords": ["product", "price-tracking", "shopping", "react", "fastapi", "python"],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT"
|
"license": "ISC",
|
||||||
|
"dependencies": {},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user