Update app

This commit is contained in:
dvirlabs 2026-05-03 04:56:48 +03:00
parent fbb3e7d850
commit 307bdef2e1
32 changed files with 380 additions and 61 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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]

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -0,0 +1,2 @@
-- Add stock column to model table
ALTER TABLE model ADD COLUMN IF NOT EXISTS stock INTEGER DEFAULT NULL;

View File

@ -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;

View File

@ -28,13 +28,14 @@ export default function ProductCard({ product }) {
<div className="price">
{product.discount_price ? (
<>
<span className="original">${product.price.toFixed(2)}</span>
<span className="discounted">${product.discount_price.toFixed(2)}</span>
<span className="original">{product.price.toFixed(2)}</span>
<span className="discounted">{product.discount_price.toFixed(2)}</span>
</>
) : (
<span>${price.toFixed(2)}</span>
<span>{price.toFixed(2)}</span>
)}
</div>
{product.stock !== null && (
<p className="stock">
{product.stock > 0 ? (
<span className="in-stock">In Stock</span>
@ -42,6 +43,7 @@ export default function ProductCard({ product }) {
<span className="out-of-stock">Out of Stock</span>
)}
</p>
)}
<Link to={`/product/${product.id}`} className="btn btn-small">
View Details
</Link>

View File

@ -40,7 +40,7 @@ export default function SearchBar() {
{results.map((product) => (
<a key={product.id} href={`/product/${product.id}`} className="search-result-item">
<span>{product.name}</span>
<span>${product.price.toFixed(2)}</span>
<span>{product.price.toFixed(2)}</span>
</a>
))}
</div>

View File

@ -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
</button>
<Link
to="/admin/models"
<button
onClick={() => 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
</Link>
</button>
</div>
{/* Products Section */}
@ -771,8 +873,8 @@ export default function Admin() {
</div>
<div className="form-group">
<label>Stock *</label>
<input type="number" name="stock" value={formData.stock} onChange={handleChange} required />
<label>Stock (Optional)</label>
<input type="number" name="stock" value={formData.stock} onChange={handleChange} placeholder="Leave empty to hide from customers" />
</div>
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
@ -936,8 +1038,8 @@ export default function Admin() {
<td style={{ padding: '0.75rem', border: '1px solid #ddd', fontSize: '0.85em', color: '#666' }}>
{product.slug || '-'}
</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>${product.price}</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.stock}</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.price}</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.stock ?? 'Hidden'}</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.is_featured ? '✓' : '-'}</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.is_on_sale ? '✓' : '-'}</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
@ -1219,6 +1321,199 @@ export default function Admin() {
</div>
</>
)}
{/* Models Section */}
{activeTab === 'models' && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
<h2>Manage Models</h2>
<button
className="btn btn-primary"
onClick={() => {
setShowModelForm(!showModelForm)
setEditingModel(null)
resetModelForm()
}}
>
{showModelForm ? 'Cancel' : '+ Add New Model'}
</button>
</div>
{showModelForm && (
<div style={{ backgroundColor: '#f5f5f5', padding: '2rem', borderRadius: '8px', marginBottom: '2rem' }}>
<h2>{editingModel ? 'Edit Model' : 'Create New Model'}</h2>
<form onSubmit={handleModelSubmit}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<div className="form-group">
<label>Model Name * (e.g., 9060, Air Max 90)</label>
<input
type="text"
name="name"
value={modelFormData.name}
onChange={handleModelChange}
placeholder="9060"
required
/>
</div>
<div className="form-group">
<label>Brand *</label>
<select
name="brand"
value={modelFormData.brand}
onChange={handleModelChange}
required
>
<option value="">Select Brand</option>
{brands.map(brand => (
<option key={brand} value={brand}>{brand}</option>
))}
</select>
</div>
<div className="form-group">
<label>Category *</label>
<select name="category_id" value={modelFormData.category_id} onChange={handleModelChange} required>
<option value="">Select Category</option>
{categories.map(category => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
</div>
<div className="form-group">
<label>Base Price (Default for all products)</label>
<input
type="number"
step="0.01"
name="base_price"
value={modelFormData.base_price}
onChange={handleModelChange}
placeholder="129.99"
/>
</div>
<div className="form-group">
<label>Stock (Optional)</label>
<input
type="number"
name="stock"
value={modelFormData.stock}
onChange={handleModelChange}
placeholder="0"
/>
</div>
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
<label>Default Sizes (comma separated)</label>
<input
type="text"
name="sizes"
value={modelFormData.sizes}
onChange={handleModelChange}
placeholder="7, 7.5, 8, 8.5, 9, 9.5, 10, 10.5, 11, 11.5, 12"
/>
</div>
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
<label>Description</label>
<textarea
name="description"
value={modelFormData.description}
onChange={handleModelChange}
rows="4"
placeholder="Model description..."
/>
</div>
</div>
<div style={{ marginTop: '1.5rem' }}>
<button type="submit" className="btn btn-primary" style={{ marginRight: '1rem' }}>
{editingModel ? 'Update Model' : 'Create Model'}
</button>
<button type="button" className="btn btn-secondary" onClick={handleModelCancel}>
Cancel
</button>
</div>
</form>
</div>
)}
<div>
<h2>Models ({models.length})</h2>
{loading ? (
<p>Loading...</p>
) : (
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '1rem' }}>
<thead>
<tr style={{ backgroundColor: '#f0f0f0', textAlign: 'left' }}>
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>ID</th>
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Model Name</th>
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Brand</th>
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Category</th>
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Base Price</th>
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Stock</th>
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Sizes</th>
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Actions</th>
</tr>
</thead>
<tbody>
{models.map(model => (
<tr key={model.id}>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{model.id}</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{model.name}</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{model.brand}</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{getCategoryName(model.category_id)}</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
{model.base_price ? `${parseFloat(model.base_price).toFixed(2)}` : '-'}
</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
{model.stock ?? '-'}
</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd', fontSize: '0.85em' }}>
{model.sizes ? model.sizes.join(', ') : '-'}
</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
<button
onClick={() => handleModelEdit(model)}
style={{
marginRight: '0.5rem',
padding: '0.25rem 0.75rem',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Edit
</button>
<button
onClick={() => handleModelDelete(model.id)}
style={{
padding: '0.25rem 0.75rem',
backgroundColor: '#f44336',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</>
)}
</div>
)
}

View File

@ -55,7 +55,7 @@ export default function Cart() {
</div>
</div>
</td>
<td>${(item.product.discount_price || item.product.price).toFixed(2)}</td>
<td>{(item.product.discount_price || item.product.price).toFixed(2)}</td>
<td>
<div className="quantity-control">
<button onClick={() => updateQuantity(index, item.quantity - 1)}></button>
@ -64,7 +64,7 @@ export default function Cart() {
</div>
</td>
<td>
${((item.product.discount_price || item.product.price) * item.quantity).toFixed(2)}
{((item.product.discount_price || item.product.price) * item.quantity).toFixed(2)}
</td>
<td>
<button
@ -85,7 +85,7 @@ export default function Cart() {
<div className="summary-rows">
<div className="summary-row">
<span>Subtotal:</span>
<span>${total.toFixed(2)}</span>
<span>{total.toFixed(2)}</span>
</div>
<div className="summary-row">
<span>Shipping:</span>
@ -93,11 +93,11 @@ export default function Cart() {
</div>
<div className="summary-row">
<span>Tax:</span>
<span>${(total * 0.1).toFixed(2)}</span>
<span>{(total * 0.1).toFixed(2)}</span>
</div>
<div className="summary-row total">
<span>Total:</span>
<span>${(total + 10 + total * 0.1).toFixed(2)}</span>
<span>{(total + 10 + total * 0.1).toFixed(2)}</span>
</div>
</div>

View File

@ -119,12 +119,12 @@ export default function Checkout() {
{cart.map((item, index) => (
<div key={index} className="summary-item">
<span>{item.product.name} x{item.quantity}</span>
<span>${((item.product.discount_price || item.product.price) * item.quantity).toFixed(2)}</span>
<span>{((item.product.discount_price || item.product.price) * item.quantity).toFixed(2)}</span>
</div>
))}
</div>
<div className="summary-total">
<strong>Total: ${(total + 10 + total * 0.1).toFixed(2)}</strong>
<strong>Total: {(total + 10 + total * 0.1).toFixed(2)}</strong>
</div>
</div>
</div>

View File

@ -285,7 +285,7 @@ export default function Models() {
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{model.brand}</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{getCategoryName(model.category_id)}</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
{model.base_price ? `$${parseFloat(model.base_price).toFixed(2)}` : '-'}
{model.base_price ? `${parseFloat(model.base_price).toFixed(2)}` : '-'}
</td>
<td style={{ padding: '0.75rem', border: '1px solid #ddd', fontSize: '0.85em' }}>
{model.sizes ? model.sizes.join(', ') : '-'}

View File

@ -58,7 +58,7 @@ export default function Orders() {
</div>
<div className="detail-row">
<span>Total Amount:</span>
<span>${order.total_amount.toFixed(2)}</span>
<span>{order.total_amount.toFixed(2)}</span>
</div>
<div className="detail-row">
<span>Items:</span>
@ -74,7 +74,7 @@ export default function Orders() {
<p>{item.product.name}</p>
<p>Qty: {item.quantity}</p>
</div>
<p>${item.price.toFixed(2)}</p>
<p>{item.price.toFixed(2)}</p>
</div>
))}
</div>

View File

@ -141,11 +141,11 @@ export default function ProductDetail() {
<div className="price">
{product.discount_price ? (
<>
<span className="original">${product.price.toFixed(2)}</span>
<span className="current">${product.discount_price.toFixed(2)}</span>
<span className="original">{product.price.toFixed(2)}</span>
<span className="current">{product.discount_price.toFixed(2)}</span>
</>
) : (
<span className="current">${price.toFixed(2)}</span>
<span className="current">{price.toFixed(2)}</span>
)}
</div>
@ -177,6 +177,7 @@ export default function ProductDetail() {
</div>
</div>
{product.stock !== null && (
<div className="stock-info">
{product.stock > 0 ? (
<span className="in-stock"> In Stock ({product.stock} available)</span>
@ -184,6 +185,7 @@ export default function ProductDetail() {
<span className="out-of-stock"> Out of Stock</span>
)}
</div>
)}
<div className="action-buttons">
<button