From eb3ae05425b3c989834837d05f3c9d2e491cdd48 Mon Sep 17 00:00:00 2001 From: lasse Date: Wed, 28 May 2025 11:01:42 +0200 Subject: [PATCH] redesing product lists --- README.md | 29 +- backend/main.py | 11 +- backend/models.py | 3 +- backend/schemas.py | 2 + database_schema.drawio | 1392 ++++++++--------- .../src/components/AddShoppingEventModal.tsx | 176 ++- frontend/src/components/ShoppingEventList.tsx | 53 +- frontend/src/types/index.ts | 2 + resources/brands.csv | 3 + resources/categories.csv | 6 + resources/database_schema.drawio | 696 +++++++++ resources/products.csv | 5 + 12 files changed, 1569 insertions(+), 809 deletions(-) create mode 100644 resources/brands.csv create mode 100644 resources/categories.csv create mode 100644 resources/database_schema.drawio create mode 100644 resources/products.csv diff --git a/README.md b/README.md index 295deb4..272205a 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ Many-to-many relationship between shopping events and products with additional d - `product_id`: Integer, Foreign key to products (required) - `amount`: Float, Quantity purchased in this event (required, > 0) - `price`: Float, Price at time of purchase (required, ≥ 0) +- `discount`: Boolean, Whether the product was purchased with a discount (default: false) #### Related Products (`related_products` table) Many-to-many self-referential relationship between products for tracking related items: @@ -180,19 +181,19 @@ Many-to-many self-referential relationship between products for tracking related │ │ • created_at │ │ │ • updated_at │ │ └─────────────────┘ - │ │ - │ │ N:M (self-referential) - │ ▼ - │ ┌─────────────────────────────┐ - │ │ Related Products │ - │ │ (Association Table) │ - │ │ │ - │ │ • id │ - │ │ • product_id │ - │ │ • related_product_id │ - │ │ • relationship_type │ - │ │ • created_at │ - │ └─────────────────────────────┘ + │ │ │ + │ │ │ N:M (self-referential) + │ │ ▼ + │ │ ┌─────────────────────────────┐ + │ │ │ Related Products │ + │ │ │ (Association Table) │ + │ │ │ │ + │ │ │ • id │ + │ │ │ • product_id │ + │ │ │ • related_product_id │ + │ │ │ • relationship_type │ + │ │ │ • created_at │ + │ │ └─────────────────────────────┘ │ │ │ │ N:M │ ▼ @@ -205,6 +206,7 @@ Many-to-many self-referential relationship between products for tracking related │ │ • product_id │ │ │ • amount │ │ │ • price │ + │ │ • discount │ │ └─────────────────────────────┘ │ │ │ │ N:1 @@ -241,6 +243,7 @@ Many-to-many self-referential relationship between products for tracking related - **Brand Tracking**: Optional brand association for products with shop availability tracking - **Related Products**: Track relationships between products (size variants, brand alternatives, similar items) - **Price History**: Each product purchase stores the price at that time, enabling price tracking +- **Discount Tracking**: Track which products were purchased with discounts for better spending analysis - **Flexible Quantities**: Support for decimal amounts (e.g., 1.5 kg of apples) - **Auto-calculation**: Total amount can be automatically calculated from individual items - **Free Items**: Supports items with price 0 (samples, promotions, etc.) diff --git a/backend/main.py b/backend/main.py index 8faeaee..c90a264 100644 --- a/backend/main.py +++ b/backend/main.py @@ -30,7 +30,7 @@ def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> s product_data = db.execute( text(""" SELECT p.id, p.name, p.organic, p.weight, p.weight_unit, - sep.amount, sep.price, + sep.amount, sep.price, sep.discount, gc.id as category_id, gc.name as category_name, gc.created_at as category_created_at, gc.updated_at as category_updated_at, b.id as brand_id, b.name as brand_name, @@ -73,7 +73,8 @@ def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> s weight=row.weight, weight_unit=row.weight_unit, amount=row.amount, - price=row.price + price=row.price, + discount=row.discount ) ) @@ -413,7 +414,8 @@ def create_shopping_event(event: schemas.ShoppingEventCreate, db: Session = Depe shopping_event_id=db_event.id, product_id=product_item.product_id, amount=product_item.amount, - price=product_item.price + price=product_item.price, + discount=product_item.discount ) ) @@ -470,7 +472,8 @@ def update_shopping_event(event_id: int, event_update: schemas.ShoppingEventCrea shopping_event_id=event_id, product_id=product_item.product_id, amount=product_item.amount, - price=product_item.price + price=product_item.price, + discount=product_item.discount ) ) diff --git a/backend/models.py b/backend/models.py index c2583f9..eedd87f 100644 --- a/backend/models.py +++ b/backend/models.py @@ -14,7 +14,8 @@ shopping_event_products = Table( Column('shopping_event_id', Integer, ForeignKey('shopping_events.id'), nullable=False), Column('product_id', Integer, ForeignKey('products.id'), nullable=False), Column('amount', Float, nullable=False), # Amount of this product bought in this event - Column('price', Float, nullable=False) # Price of this product at the time of this shopping event + Column('price', Float, nullable=False), # Price of this product at the time of this shopping event + Column('discount', Boolean, default=False, nullable=False) # Whether this product was purchased with a discount ) # Association table for many-to-many self-referential relationship between related products diff --git a/backend/schemas.py b/backend/schemas.py index 75fef18..6f3ba4f 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -117,6 +117,7 @@ class ProductInEvent(BaseModel): product_id: int amount: float = Field(..., gt=0) price: float = Field(..., ge=0) # Price at the time of this shopping event (allow free items) + discount: bool = False # Whether this product was purchased with a discount class ProductWithEventData(BaseModel): id: int @@ -128,6 +129,7 @@ class ProductWithEventData(BaseModel): weight_unit: str amount: float # Amount purchased in this event price: float # Price at the time of this event + discount: bool # Whether this product was purchased with a discount class Config: from_attributes = True diff --git a/database_schema.drawio b/database_schema.drawio index 24fd330..190476b 100644 --- a/database_schema.drawio +++ b/database_schema.drawio @@ -1,696 +1,696 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/AddShoppingEventModal.tsx b/frontend/src/components/AddShoppingEventModal.tsx index ee7407a..c395389 100644 --- a/frontend/src/components/AddShoppingEventModal.tsx +++ b/frontend/src/components/AddShoppingEventModal.tsx @@ -35,7 +35,8 @@ const AddShoppingEventModal: React.FC = ({ const [newProductItem, setNewProductItem] = useState({ product_id: 0, amount: 1, - price: 0 + price: 0, + discount: false }); const [autoCalculate, setAutoCalculate] = useState(true); @@ -58,7 +59,8 @@ const AddShoppingEventModal: React.FC = ({ const mappedProducts = editEvent.products.map(p => ({ product_id: p.id, amount: p.amount, - price: p.price + price: p.price, + discount: p.discount })); // Calculate the sum of all products @@ -220,7 +222,7 @@ const AddShoppingEventModal: React.FC = ({ const addProductToEvent = () => { if (newProductItem.product_id > 0 && newProductItem.amount > 0 && newProductItem.price >= 0) { setSelectedProducts([...selectedProducts, { ...newProductItem }]); - setNewProductItem({ product_id: 0, amount: 1, price: 0 }); + setNewProductItem({ product_id: 0, amount: 1, price: 0, discount: false }); } }; @@ -234,7 +236,8 @@ const AddShoppingEventModal: React.FC = ({ setNewProductItem({ product_id: productToEdit.product_id, amount: productToEdit.amount, - price: productToEdit.price + price: productToEdit.price, + discount: productToEdit.discount }); // Remove the item from the selected list setSelectedProducts(selectedProducts.filter((_, i) => i !== index)); @@ -246,7 +249,8 @@ const AddShoppingEventModal: React.FC = ({ const weightInfo = product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit; const organicEmoji = product.organic ? ' 🌱' : ''; - return `${product.name}${organicEmoji} ${weightInfo}`; + const brandInfo = product.brand ? ` (${product.brand.name})` : ''; + return `${product.name}${organicEmoji} ${weightInfo}${brandInfo}`; }; // Filter products based on selected shop's brands @@ -297,38 +301,38 @@ const AddShoppingEventModal: React.FC = ({ )}
- {/* Shop Selection */} -
- - -
- - {/* Date */} -
- - setFormData({...formData, date: 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" - required - /> + {/* Shop and Date Selection */} +
+
+ + +
+
+ + setFormData({...formData, date: e.target.value})} + className="w-full h-10 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" + required + /> +
{/* Add Products Section */} @@ -344,7 +348,7 @@ const AddShoppingEventModal: React.FC = ({ setNewProductItem({...newProductItem, discount: e.target.checked})} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + /> +
+ +
+ @@ -423,32 +443,52 @@ const AddShoppingEventModal: React.FC = ({ {selectedProducts.length > 0 && (

Selected Items:

- {selectedProducts.map((item, index) => ( -
-
-
- {getProductName(item.product_id)} -
-
- {item.amount} × ${item.price.toFixed(2)} = ${(item.amount * item.price).toFixed(2)} -
-
-
- - + {Object.entries( + selectedProducts.reduce((groups, item, index) => { + const product = products.find(p => p.id === item.product_id); + const category = product?.category.name || 'Unknown'; + if (!groups[category]) { + groups[category] = []; + } + groups[category].push({ ...item, index }); + return groups; + }, {} as Record) + ) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([category, categoryItems]) => ( +
+
+ {category}
+ {categoryItems.map((item) => ( +
+
+
+ {getProductName(item.product_id)} + + {item.amount} × ${item.price.toFixed(2)} = ${(item.amount * item.price).toFixed(2)} + {item.discount && 🏷️} + +
+
+
+ + +
+
+ ))}
))}
diff --git a/frontend/src/components/ShoppingEventList.tsx b/frontend/src/components/ShoppingEventList.tsx index 07f27e0..48caa83 100644 --- a/frontend/src/components/ShoppingEventList.tsx +++ b/frontend/src/components/ShoppingEventList.tsx @@ -431,34 +431,33 @@ const ShoppingEventList: React.FC = () => { onMouseEnter={() => setShowItemsPopup(true)} onMouseLeave={handleItemsLeave} > -
- {hoveredEvent.products.map((product, index) => ( -
-
-
-
- {product.name} {product.organic ? '🌱' : ''} +
+ {Object.entries( + hoveredEvent.products.reduce((groups, product) => { + const category = product.category?.name || 'Unknown'; + if (!groups[category]) { + groups[category] = []; + } + groups[category].push(product); + return groups; + }, {} as Record) + ) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([category, categoryProducts]) => ( +
+
+ {category} +
+
+ {categoryProducts.map((product, index) => ( +
+ {product.name} {product.organic ? '🌱' : ''} {product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit} + + {product.amount} × ${product.price.toFixed(2)} = ${(product.amount * product.price).toFixed(2)} + {product.discount && 🏷️} +
-
- {product.category?.name || 'Unknown category'} -
- {product.brand && ( -
- Brand: {product.brand.name} -
- )} -
-
-
- ${product.price.toFixed(2)} -
-
- Qty: {product.amount} -
-
- ${(product.amount * product.price).toFixed(2)} -
-
+ ))}
))} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index da76c8e..81a3caf 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -62,6 +62,7 @@ export interface ProductInEvent { product_id: number; amount: number; price: number; + discount: boolean; } export interface ProductWithEventData { @@ -74,6 +75,7 @@ export interface ProductWithEventData { weight_unit: string; amount: number; price: number; + discount: boolean; } export interface ShoppingEvent { diff --git a/resources/brands.csv b/resources/brands.csv new file mode 100644 index 0000000..1e801f9 --- /dev/null +++ b/resources/brands.csv @@ -0,0 +1,3 @@ +name +denree +Rewe Bio \ No newline at end of file diff --git a/resources/categories.csv b/resources/categories.csv new file mode 100644 index 0000000..fad1dcb --- /dev/null +++ b/resources/categories.csv @@ -0,0 +1,6 @@ +name +Obst +Gemüse +Konserven +Tiefkühl +Molkereiprodukte \ No newline at end of file diff --git a/resources/database_schema.drawio b/resources/database_schema.drawio new file mode 100644 index 0000000..24fd330 --- /dev/null +++ b/resources/database_schema.drawio @@ -0,0 +1,696 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/products.csv b/resources/products.csv new file mode 100644 index 0000000..fd4ea4a --- /dev/null +++ b/resources/products.csv @@ -0,0 +1,5 @@ +name,category_name,organic,brand_name,weight,weight_unit +"Milch 3,5%",Molkereiprodukte,true,denree,1,l +"Milch 3,5%",Molkereiprodukte,true,Rewe Bio,1,l +"Frischkäse Natur",Molkereiprodukte,true,Rewe Bio,175,g +"Frischkäse Kräuter",Molkereiprodukte,true,Rewe Bio,175,g \ No newline at end of file