diff --git a/backend/app/models/__pycache__/model.cpython-314.pyc b/backend/app/models/__pycache__/model.cpython-314.pyc index 0632483..9da8f69 100644 Binary files a/backend/app/models/__pycache__/model.cpython-314.pyc and b/backend/app/models/__pycache__/model.cpython-314.pyc differ diff --git a/backend/app/models/__pycache__/product.cpython-314.pyc b/backend/app/models/__pycache__/product.cpython-314.pyc index f6cedc7..151e4dd 100644 Binary files a/backend/app/models/__pycache__/product.cpython-314.pyc and b/backend/app/models/__pycache__/product.cpython-314.pyc differ diff --git a/backend/app/models/model.py b/backend/app/models/model.py index 05f1aa7..0fde269 100644 --- a/backend/app/models/model.py +++ b/backend/app/models/model.py @@ -13,6 +13,7 @@ class Model(Base): brand = Column(String(100), nullable=False) base_price = Column(DECIMAL(10, 2), nullable=True) sizes = Column(JSON, nullable=True) + stock = Column(Integer, nullable=True) description = Column(String, nullable=True) created_at = Column(TIMESTAMP, default=datetime.utcnow) diff --git a/backend/app/models/product.py b/backend/app/models/product.py index e068d9a..bc97490 100644 --- a/backend/app/models/product.py +++ b/backend/app/models/product.py @@ -19,7 +19,7 @@ class Product(Base): brand = Column(String) sizes = Column(JSON) # ["S", "M", "L", "XL", ...] colors = Column(JSON) # ["Red", "Blue", ...] - stock = Column(Integer, default=0) + stock = Column(Integer, nullable=True, default=None) images = Column(JSON) # Array of image URLs is_featured = Column(Boolean, default=False) is_on_sale = Column(Boolean, default=False) diff --git a/backend/app/routers/__pycache__/categories.cpython-314.pyc b/backend/app/routers/__pycache__/categories.cpython-314.pyc index dc2d0e9..a46a785 100644 Binary files a/backend/app/routers/__pycache__/categories.cpython-314.pyc and b/backend/app/routers/__pycache__/categories.cpython-314.pyc differ diff --git a/backend/app/routers/__pycache__/contact.cpython-314.pyc b/backend/app/routers/__pycache__/contact.cpython-314.pyc index 906e587..079ec25 100644 Binary files a/backend/app/routers/__pycache__/contact.cpython-314.pyc and b/backend/app/routers/__pycache__/contact.cpython-314.pyc differ diff --git a/backend/app/routers/__pycache__/models.cpython-314.pyc b/backend/app/routers/__pycache__/models.cpython-314.pyc index 74556f0..073a560 100644 Binary files a/backend/app/routers/__pycache__/models.cpython-314.pyc and b/backend/app/routers/__pycache__/models.cpython-314.pyc differ diff --git a/backend/app/routers/__pycache__/users.cpython-314.pyc b/backend/app/routers/__pycache__/users.cpython-314.pyc index 73554f4..ea95dfc 100644 Binary files a/backend/app/routers/__pycache__/users.cpython-314.pyc and b/backend/app/routers/__pycache__/users.cpython-314.pyc differ diff --git a/backend/app/routers/categories.py b/backend/app/routers/categories.py index ef8cb73..c9e92d8 100644 --- a/backend/app/routers/categories.py +++ b/backend/app/routers/categories.py @@ -28,7 +28,8 @@ def create_category( db: Session = Depends(get_db), admin: User = Depends(get_current_admin_user) ): - db_category = Category(**category.dict()) + category_data = category.model_dump() if hasattr(category, 'model_dump') else category.dict() + db_category = Category(**category_data) db.add(db_category) db.commit() db.refresh(db_category) @@ -46,7 +47,8 @@ def update_category( if not category: raise HTTPException(status_code=404, detail="Category not found") - for field, value in category_update.dict(exclude_unset=True).items(): + update_data = category_update.model_dump(exclude_unset=True) if hasattr(category_update, 'model_dump') else category_update.dict(exclude_unset=True) + for field, value in update_data.items(): setattr(category, field, value) db.commit() diff --git a/backend/app/routers/contact.py b/backend/app/routers/contact.py index 2084216..d1f0094 100644 --- a/backend/app/routers/contact.py +++ b/backend/app/routers/contact.py @@ -9,7 +9,8 @@ router = APIRouter(prefix="/api/contact", tags=["contact"]) @router.post("", response_model=ContactMessageResponse) def send_contact_message(message: ContactMessageCreate, db: Session = Depends(get_db)): - db_message = ContactMessage(**message.dict()) + message_data = message.model_dump() if hasattr(message, 'model_dump') else message.dict() + db_message = ContactMessage(**message_data) db.add(db_message) db.commit() db.refresh(db_message) diff --git a/backend/app/routers/models.py b/backend/app/routers/models.py index 6ee78d4..b803340 100644 --- a/backend/app/routers/models.py +++ b/backend/app/routers/models.py @@ -55,7 +55,8 @@ def create_model( detail=f"Model '{model_data.name}' already exists for brand '{model_data.brand}' in this category" ) - db_model = Model(**model_data.dict()) + model_dict = model_data.model_dump() if hasattr(model_data, 'model_dump') else model_data.dict() + db_model = Model(**model_dict) db.add(db_model) db.commit() db.refresh(db_model) @@ -74,7 +75,8 @@ def update_model( if not model: raise HTTPException(status_code=404, detail="Model not found") - for field, value in model_update.dict(exclude_unset=True).items(): + update_data = model_update.model_dump(exclude_unset=True) if hasattr(model_update, 'model_dump') else model_update.dict(exclude_unset=True) + for field, value in update_data.items(): setattr(model, field, value) db.commit() diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py index 6f678b2..8dd0bc4 100644 --- a/backend/app/routers/users.py +++ b/backend/app/routers/users.py @@ -21,14 +21,16 @@ def update_user_profile( current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): - for field, value in user_update.dict(exclude_unset=True).items(): + update_data = user_update.model_dump(exclude_unset=True) if hasattr(user_update, 'model_dump') else user_update.dict(exclude_unset=True) + for field, value in update_data.items(): setattr(current_user, field, value) db.commit() db.refresh(current_user) return current_user - for field, value in user_update.dict(exclude_unset=True).items(): + update_data2 = user_update.model_dump(exclude_unset=True) if hasattr(user_update, 'model_dump') else user_update.dict(exclude_unset=True) + for field, value in update_data2.items(): setattr(user, field, value) db.commit() diff --git a/backend/app/schemas/__pycache__/model.cpython-314.pyc b/backend/app/schemas/__pycache__/model.cpython-314.pyc index eee4462..d8405e2 100644 Binary files a/backend/app/schemas/__pycache__/model.cpython-314.pyc and b/backend/app/schemas/__pycache__/model.cpython-314.pyc differ diff --git a/backend/app/schemas/__pycache__/product.cpython-314.pyc b/backend/app/schemas/__pycache__/product.cpython-314.pyc index e8e6a81..d857622 100644 Binary files a/backend/app/schemas/__pycache__/product.cpython-314.pyc and b/backend/app/schemas/__pycache__/product.cpython-314.pyc differ diff --git a/backend/app/schemas/model.py b/backend/app/schemas/model.py index 722f59d..0fc3de4 100644 --- a/backend/app/schemas/model.py +++ b/backend/app/schemas/model.py @@ -10,6 +10,7 @@ class ModelCreate(BaseModel): brand: str base_price: Optional[Decimal] = None sizes: Optional[List[str]] = [] + stock: Optional[int] = None description: Optional[str] = None @@ -19,6 +20,7 @@ class ModelUpdate(BaseModel): brand: Optional[str] = None base_price: Optional[Decimal] = None sizes: Optional[List[str]] = None + stock: Optional[int] = None description: Optional[str] = None @@ -29,6 +31,7 @@ class ModelResponse(BaseModel): brand: str base_price: Optional[Decimal] sizes: Optional[List[str]] + stock: Optional[int] description: Optional[str] created_at: datetime diff --git a/backend/app/schemas/product.py b/backend/app/schemas/product.py index 6015b63..7e2029a 100644 --- a/backend/app/schemas/product.py +++ b/backend/app/schemas/product.py @@ -14,10 +14,10 @@ class ProductCreate(BaseModel): model_id: Optional[int] = None gender: str # men, women brand: str - sizes: List[str] + sizes: Optional[List[str]] = [] colors: Optional[List[str]] = [] - stock: int - images: List[str] + stock: Optional[int] = None + images: Optional[List[str]] = [] is_featured: bool = False is_on_sale: bool = False override_price: Optional[Decimal] = None @@ -55,10 +55,10 @@ class ProductResponse(BaseModel): model_id: Optional[int] gender: str brand: str - sizes: List[str] + sizes: Optional[List[str]] colors: Optional[List[str]] - stock: int - images: List[str] + stock: Optional[int] + images: Optional[List[str]] is_featured: bool is_on_sale: bool override_price: Optional[Decimal] diff --git a/backend/app/services/__pycache__/cart.cpython-314.pyc b/backend/app/services/__pycache__/cart.cpython-314.pyc index 038f8c2..ded38cb 100644 Binary files a/backend/app/services/__pycache__/cart.cpython-314.pyc and b/backend/app/services/__pycache__/cart.cpython-314.pyc differ diff --git a/backend/app/services/__pycache__/order.cpython-314.pyc b/backend/app/services/__pycache__/order.cpython-314.pyc index f6681f4..4604d3e 100644 Binary files a/backend/app/services/__pycache__/order.cpython-314.pyc and b/backend/app/services/__pycache__/order.cpython-314.pyc differ diff --git a/backend/app/services/__pycache__/product.cpython-314.pyc b/backend/app/services/__pycache__/product.cpython-314.pyc index 885da87..52fc964 100644 Binary files a/backend/app/services/__pycache__/product.cpython-314.pyc and b/backend/app/services/__pycache__/product.cpython-314.pyc differ diff --git a/backend/app/services/cart.py b/backend/app/services/cart.py index 1ce53ee..0dfcd01 100644 --- a/backend/app/services/cart.py +++ b/backend/app/services/cart.py @@ -36,7 +36,8 @@ def add_to_cart(db: Session, user_id: int, item: CartItemCreate) -> CartItem: db.refresh(existing_item) return existing_item - cart_item = CartItem(cart_id=cart.id, **item.dict()) + item_data = item.model_dump() if hasattr(item, 'model_dump') else item.dict() + cart_item = CartItem(cart_id=cart.id, **item_data) db.add(cart_item) db.commit() db.refresh(cart_item) diff --git a/backend/app/services/order.py b/backend/app/services/order.py index 18e72bc..bb0ce0d 100644 --- a/backend/app/services/order.py +++ b/backend/app/services/order.py @@ -29,12 +29,13 @@ def create_order(db: Session, user_id: int, order_data: OrderCreate) -> Optional order_number = f"ORD-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6].upper()}" + order_dict = order_data.model_dump() if hasattr(order_data, 'model_dump') else order_data.dict() order = Order( user_id=user_id, order_number=order_number, status="pending", total_amount=total_amount, - **order_data.dict(), + **order_dict, ) db.add(order) diff --git a/backend/app/services/product.py b/backend/app/services/product.py index 03c1dd6..731aeed 100644 --- a/backend/app/services/product.py +++ b/backend/app/services/product.py @@ -32,7 +32,8 @@ def get_product_by_id(db: Session, product_id: int) -> Optional[Product]: def create_product(db: Session, product: ProductCreate) -> Product: - db_product = Product(**product.dict()) + product_data = product.model_dump() if hasattr(product, 'model_dump') else product.dict() + db_product = Product(**product_data) db.add(db_product) db.commit() db.refresh(db_product) @@ -44,7 +45,8 @@ def update_product(db: Session, product_id: int, product_update: ProductUpdate) if not db_product: return None - for field, value in product_update.dict(exclude_unset=True).items(): + update_data = product_update.model_dump(exclude_unset=True) if hasattr(product_update, 'model_dump') else product_update.dict(exclude_unset=True) + for field, value in update_data.items(): setattr(db_product, field, value) db.commit() diff --git a/backend/migrations/003_add_stock_to_model.sql b/backend/migrations/003_add_stock_to_model.sql new file mode 100644 index 0000000..c3d5929 --- /dev/null +++ b/backend/migrations/003_add_stock_to_model.sql @@ -0,0 +1,2 @@ +-- Add stock column to model table +ALTER TABLE model ADD COLUMN IF NOT EXISTS stock INTEGER DEFAULT NULL; diff --git a/backend/migrations/004_make_product_stock_nullable.sql b/backend/migrations/004_make_product_stock_nullable.sql new file mode 100644 index 0000000..2438639 --- /dev/null +++ b/backend/migrations/004_make_product_stock_nullable.sql @@ -0,0 +1,3 @@ +-- Make product stock column nullable (allow hiding stock from customers) +ALTER TABLE product ALTER COLUMN stock DROP DEFAULT; +ALTER TABLE product ALTER COLUMN stock DROP NOT NULL; diff --git a/frontend/src/components/ProductCard.jsx b/frontend/src/components/ProductCard.jsx index 533b3bb..f03d8ee 100644 --- a/frontend/src/components/ProductCard.jsx +++ b/frontend/src/components/ProductCard.jsx @@ -28,20 +28,22 @@ export default function ProductCard({ product }) {
- {product.stock > 0 ? ( - In Stock - ) : ( - Out of Stock - )} -
+ {product.stock !== null && ( ++ {product.stock > 0 ? ( + In Stock + ) : ( + Out of Stock + )} +
+ )} View Details diff --git a/frontend/src/components/SearchBar.jsx b/frontend/src/components/SearchBar.jsx index 8c87531..56fbec2 100644 --- a/frontend/src/components/SearchBar.jsx +++ b/frontend/src/components/SearchBar.jsx @@ -40,7 +40,7 @@ export default function SearchBar() { {results.map((product) => ( {product.name} - ${product.price.toFixed(2)} + ₪{product.price.toFixed(2)} ))} diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index 970d141..dbced76 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -52,6 +52,17 @@ export default function Admin() { const [editingBrand, setEditingBrand] = useState(null) const [brandFormData, setBrandFormData] = useState({ name: '' }) const [brandsList, setBrandsList] = useState([]) // Separate list for brand management + const [showModelForm, setShowModelForm] = useState(false) + const [editingModel, setEditingModel] = useState(null) + const [modelFormData, setModelFormData] = useState({ + name: '', + category_id: '', + brand: '', + base_price: '', + sizes: '', + stock: '', + description: '', + }) // Redirect if not admin useEffect(() => { @@ -201,8 +212,8 @@ export default function Admin() { discount_price: formData.discount_price ? parseFloat(formData.discount_price) : null, category_id: parseInt(formData.category_id), model_id: formData.model_id ? parseInt(formData.model_id) : null, - stock: parseInt(formData.stock), - sizes: formData.sizes ? formData.sizes.split(',').map(s => s.trim()) : null, + stock: formData.stock ? parseInt(formData.stock) : null, + sizes: formData.sizes ? formData.sizes.split(',').map(s => s.trim()).filter(s => s) : [], images: formData.images.split(',').map(i => i.trim()).filter(i => i), override_price: formData.override_price ? parseFloat(formData.override_price) : null, override_sizes: formData.override_sizes ? formData.override_sizes.split(',').map(s => s.trim()) : null, @@ -505,6 +516,94 @@ export default function Admin() { } } + // Model management functions + const handleModelChange = (e) => { + const { name, value } = e.target + setModelFormData({ ...modelFormData, [name]: value }) + } + + const handleModelSubmit = async (e) => { + e.preventDefault() + + const modelData = { + ...modelFormData, + category_id: parseInt(modelFormData.category_id), + base_price: modelFormData.base_price ? parseFloat(modelFormData.base_price) : null, + sizes: modelFormData.sizes ? modelFormData.sizes.split(',').map(s => s.trim()) : [], + stock: modelFormData.stock ? parseInt(modelFormData.stock) : null, + } + + try { + if (editingModel) { + await api.put(`/models/${editingModel.id}`, modelData) + alert('Model updated successfully!') + } else { + await api.post('/models', modelData) + alert('Model created successfully!') + } + setShowModelForm(false) + setEditingModel(null) + resetModelForm() + fetchModels() + } catch (error) { + console.error('Error saving model:', error) + alert('Error saving model: ' + (error.response?.data?.detail || 'Unknown error')) + } + } + + const handleModelEdit = (model) => { + setEditingModel(model) + setModelFormData({ + name: model.name || '', + category_id: model.category_id || '', + brand: model.brand || '', + base_price: model.base_price || '', + sizes: Array.isArray(model.sizes) ? model.sizes.join(', ') : '', + stock: model.stock || '', + description: model.description || '', + }) + setShowModelForm(true) + setTimeout(() => { + window.scrollTo({ top: 0, behavior: 'smooth' }) + }, 100) + } + + const handleModelDelete = async (id) => { + if (!confirm('Are you sure you want to delete this model? This will unlink all associated products.')) return + + try { + await api.delete(`/models/${id}`) + alert('Model deleted successfully!') + fetchModels() + } catch (error) { + console.error('Error deleting model:', error) + alert('Error deleting model: ' + (error.response?.data?.detail || 'Unknown error')) + } + } + + const resetModelForm = () => { + setModelFormData({ + name: '', + category_id: '', + brand: '', + base_price: '', + sizes: '', + stock: '', + description: '', + }) + } + + const handleModelCancel = () => { + setShowModelForm(false) + setEditingModel(null) + resetModelForm() + } + + const getCategoryName = (categoryId) => { + const category = categories.find(c => c.id === categoryId) + return category ? category.name : 'Unknown' + } + if (!user?.is_admin) { return null } @@ -560,18 +659,21 @@ export default function Admin() { > Brands - setActiveTab('models')} style={{ - display: 'inline-block', padding: '0.75rem 1.5rem', - textDecoration: 'none', - color: '#333', + marginRight: '0.5rem', + border: 'none', + borderBottom: activeTab === 'models' ? '3px solid #007bff' : '3px solid transparent', + backgroundColor: 'transparent', + cursor: 'pointer', + fontWeight: activeTab === 'models' ? 'bold' : 'normal', fontSize: '1rem' }} > Models - + {/* Products Section */} @@ -771,8 +873,8 @@ export default function Admin() {Loading...
+ ) : ( +| ID | +Model Name | +Brand | +Category | +Base Price | +Stock | +Sizes | +Actions | +
|---|---|---|---|---|---|---|---|
| {model.id} | +{model.name} | +{model.brand} | +{getCategoryName(model.category_id)} | ++ {model.base_price ? `₪${parseFloat(model.base_price).toFixed(2)}` : '-'} + | ++ {model.stock ?? '-'} + | ++ {model.sizes ? model.sizes.join(', ') : '-'} + | ++ + + | +