Update app
This commit is contained in:
parent
fbb3e7d850
commit
307bdef2e1
Binary file not shown.
Binary file not shown.
@ -13,6 +13,7 @@ class Model(Base):
|
|||||||
brand = Column(String(100), nullable=False)
|
brand = Column(String(100), nullable=False)
|
||||||
base_price = Column(DECIMAL(10, 2), nullable=True)
|
base_price = Column(DECIMAL(10, 2), nullable=True)
|
||||||
sizes = Column(JSON, nullable=True)
|
sizes = Column(JSON, nullable=True)
|
||||||
|
stock = Column(Integer, nullable=True)
|
||||||
description = Column(String, nullable=True)
|
description = Column(String, nullable=True)
|
||||||
created_at = Column(TIMESTAMP, default=datetime.utcnow)
|
created_at = Column(TIMESTAMP, default=datetime.utcnow)
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ class Product(Base):
|
|||||||
brand = Column(String)
|
brand = Column(String)
|
||||||
sizes = Column(JSON) # ["S", "M", "L", "XL", ...]
|
sizes = Column(JSON) # ["S", "M", "L", "XL", ...]
|
||||||
colors = Column(JSON) # ["Red", "Blue", ...]
|
colors = Column(JSON) # ["Red", "Blue", ...]
|
||||||
stock = Column(Integer, default=0)
|
stock = Column(Integer, nullable=True, default=None)
|
||||||
images = Column(JSON) # Array of image URLs
|
images = Column(JSON) # Array of image URLs
|
||||||
is_featured = Column(Boolean, default=False)
|
is_featured = Column(Boolean, default=False)
|
||||||
is_on_sale = Column(Boolean, default=False)
|
is_on_sale = Column(Boolean, default=False)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -28,7 +28,8 @@ def create_category(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
admin: User = Depends(get_current_admin_user)
|
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.add(db_category)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_category)
|
db.refresh(db_category)
|
||||||
@ -46,7 +47,8 @@ def update_category(
|
|||||||
if not category:
|
if not category:
|
||||||
raise HTTPException(status_code=404, detail="Category not found")
|
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)
|
setattr(category, field, value)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@ -9,7 +9,8 @@ router = APIRouter(prefix="/api/contact", tags=["contact"])
|
|||||||
|
|
||||||
@router.post("", response_model=ContactMessageResponse)
|
@router.post("", response_model=ContactMessageResponse)
|
||||||
def send_contact_message(message: ContactMessageCreate, db: Session = Depends(get_db)):
|
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.add(db_message)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_message)
|
db.refresh(db_message)
|
||||||
|
|||||||
@ -55,7 +55,8 @@ def create_model(
|
|||||||
detail=f"Model '{model_data.name}' already exists for brand '{model_data.brand}' in this category"
|
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.add(db_model)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_model)
|
db.refresh(db_model)
|
||||||
@ -74,7 +75,8 @@ def update_model(
|
|||||||
if not model:
|
if not model:
|
||||||
raise HTTPException(status_code=404, detail="Model not found")
|
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)
|
setattr(model, field, value)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@ -21,14 +21,16 @@ def update_user_profile(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db),
|
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)
|
setattr(current_user, field, value)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(current_user)
|
db.refresh(current_user)
|
||||||
return 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)
|
setattr(user, field, value)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -10,6 +10,7 @@ class ModelCreate(BaseModel):
|
|||||||
brand: str
|
brand: str
|
||||||
base_price: Optional[Decimal] = None
|
base_price: Optional[Decimal] = None
|
||||||
sizes: Optional[List[str]] = []
|
sizes: Optional[List[str]] = []
|
||||||
|
stock: Optional[int] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ class ModelUpdate(BaseModel):
|
|||||||
brand: Optional[str] = None
|
brand: Optional[str] = None
|
||||||
base_price: Optional[Decimal] = None
|
base_price: Optional[Decimal] = None
|
||||||
sizes: Optional[List[str]] = None
|
sizes: Optional[List[str]] = None
|
||||||
|
stock: Optional[int] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@ -29,6 +31,7 @@ class ModelResponse(BaseModel):
|
|||||||
brand: str
|
brand: str
|
||||||
base_price: Optional[Decimal]
|
base_price: Optional[Decimal]
|
||||||
sizes: Optional[List[str]]
|
sizes: Optional[List[str]]
|
||||||
|
stock: Optional[int]
|
||||||
description: Optional[str]
|
description: Optional[str]
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
|
|||||||
@ -14,10 +14,10 @@ class ProductCreate(BaseModel):
|
|||||||
model_id: Optional[int] = None
|
model_id: Optional[int] = None
|
||||||
gender: str # men, women
|
gender: str # men, women
|
||||||
brand: str
|
brand: str
|
||||||
sizes: List[str]
|
sizes: Optional[List[str]] = []
|
||||||
colors: Optional[List[str]] = []
|
colors: Optional[List[str]] = []
|
||||||
stock: int
|
stock: Optional[int] = None
|
||||||
images: List[str]
|
images: Optional[List[str]] = []
|
||||||
is_featured: bool = False
|
is_featured: bool = False
|
||||||
is_on_sale: bool = False
|
is_on_sale: bool = False
|
||||||
override_price: Optional[Decimal] = None
|
override_price: Optional[Decimal] = None
|
||||||
@ -55,10 +55,10 @@ class ProductResponse(BaseModel):
|
|||||||
model_id: Optional[int]
|
model_id: Optional[int]
|
||||||
gender: str
|
gender: str
|
||||||
brand: str
|
brand: str
|
||||||
sizes: List[str]
|
sizes: Optional[List[str]]
|
||||||
colors: Optional[List[str]]
|
colors: Optional[List[str]]
|
||||||
stock: int
|
stock: Optional[int]
|
||||||
images: List[str]
|
images: Optional[List[str]]
|
||||||
is_featured: bool
|
is_featured: bool
|
||||||
is_on_sale: bool
|
is_on_sale: bool
|
||||||
override_price: Optional[Decimal]
|
override_price: Optional[Decimal]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -36,7 +36,8 @@ def add_to_cart(db: Session, user_id: int, item: CartItemCreate) -> CartItem:
|
|||||||
db.refresh(existing_item)
|
db.refresh(existing_item)
|
||||||
return 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.add(cart_item)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(cart_item)
|
db.refresh(cart_item)
|
||||||
|
|||||||
@ -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_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(
|
order = Order(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
order_number=order_number,
|
order_number=order_number,
|
||||||
status="pending",
|
status="pending",
|
||||||
total_amount=total_amount,
|
total_amount=total_amount,
|
||||||
**order_data.dict(),
|
**order_dict,
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(order)
|
db.add(order)
|
||||||
|
|||||||
@ -32,7 +32,8 @@ def get_product_by_id(db: Session, product_id: int) -> Optional[Product]:
|
|||||||
|
|
||||||
|
|
||||||
def create_product(db: Session, product: ProductCreate) -> 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.add(db_product)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_product)
|
db.refresh(db_product)
|
||||||
@ -44,7 +45,8 @@ def update_product(db: Session, product_id: int, product_update: ProductUpdate)
|
|||||||
if not db_product:
|
if not db_product:
|
||||||
return None
|
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)
|
setattr(db_product, field, value)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
2
backend/migrations/003_add_stock_to_model.sql
Normal file
2
backend/migrations/003_add_stock_to_model.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add stock column to model table
|
||||||
|
ALTER TABLE model ADD COLUMN IF NOT EXISTS stock INTEGER DEFAULT NULL;
|
||||||
3
backend/migrations/004_make_product_stock_nullable.sql
Normal file
3
backend/migrations/004_make_product_stock_nullable.sql
Normal 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;
|
||||||
@ -28,13 +28,14 @@ export default function ProductCard({ product }) {
|
|||||||
<div className="price">
|
<div className="price">
|
||||||
{product.discount_price ? (
|
{product.discount_price ? (
|
||||||
<>
|
<>
|
||||||
<span className="original">${product.price.toFixed(2)}</span>
|
<span className="original">₪{product.price.toFixed(2)}</span>
|
||||||
<span className="discounted">${product.discount_price.toFixed(2)}</span>
|
<span className="discounted">₪{product.discount_price.toFixed(2)}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span>${price.toFixed(2)}</span>
|
<span>₪{price.toFixed(2)}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{product.stock !== null && (
|
||||||
<p className="stock">
|
<p className="stock">
|
||||||
{product.stock > 0 ? (
|
{product.stock > 0 ? (
|
||||||
<span className="in-stock">In Stock</span>
|
<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>
|
<span className="out-of-stock">Out of Stock</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
<Link to={`/product/${product.id}`} className="btn btn-small">
|
<Link to={`/product/${product.id}`} className="btn btn-small">
|
||||||
View Details
|
View Details
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export default function SearchBar() {
|
|||||||
{results.map((product) => (
|
{results.map((product) => (
|
||||||
<a key={product.id} href={`/product/${product.id}`} className="search-result-item">
|
<a key={product.id} href={`/product/${product.id}`} className="search-result-item">
|
||||||
<span>{product.name}</span>
|
<span>{product.name}</span>
|
||||||
<span>${product.price.toFixed(2)}</span>
|
<span>₪{product.price.toFixed(2)}</span>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -52,6 +52,17 @@ export default function Admin() {
|
|||||||
const [editingBrand, setEditingBrand] = useState(null)
|
const [editingBrand, setEditingBrand] = useState(null)
|
||||||
const [brandFormData, setBrandFormData] = useState({ name: '' })
|
const [brandFormData, setBrandFormData] = useState({ name: '' })
|
||||||
const [brandsList, setBrandsList] = useState([]) // Separate list for brand management
|
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
|
// Redirect if not admin
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -201,8 +212,8 @@ export default function Admin() {
|
|||||||
discount_price: formData.discount_price ? parseFloat(formData.discount_price) : null,
|
discount_price: formData.discount_price ? parseFloat(formData.discount_price) : null,
|
||||||
category_id: parseInt(formData.category_id),
|
category_id: parseInt(formData.category_id),
|
||||||
model_id: formData.model_id ? parseInt(formData.model_id) : null,
|
model_id: formData.model_id ? parseInt(formData.model_id) : null,
|
||||||
stock: parseInt(formData.stock),
|
stock: formData.stock ? parseInt(formData.stock) : null,
|
||||||
sizes: formData.sizes ? formData.sizes.split(',').map(s => s.trim()) : 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),
|
images: formData.images.split(',').map(i => i.trim()).filter(i => i),
|
||||||
override_price: formData.override_price ? parseFloat(formData.override_price) : null,
|
override_price: formData.override_price ? parseFloat(formData.override_price) : null,
|
||||||
override_sizes: formData.override_sizes ? formData.override_sizes.split(',').map(s => s.trim()) : 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) {
|
if (!user?.is_admin) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -560,18 +659,21 @@ export default function Admin() {
|
|||||||
>
|
>
|
||||||
Brands
|
Brands
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<button
|
||||||
to="/admin/models"
|
onClick={() => setActiveTab('models')}
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-block',
|
|
||||||
padding: '0.75rem 1.5rem',
|
padding: '0.75rem 1.5rem',
|
||||||
textDecoration: 'none',
|
marginRight: '0.5rem',
|
||||||
color: '#333',
|
border: 'none',
|
||||||
|
borderBottom: activeTab === 'models' ? '3px solid #007bff' : '3px solid transparent',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontWeight: activeTab === 'models' ? 'bold' : 'normal',
|
||||||
fontSize: '1rem'
|
fontSize: '1rem'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Models
|
Models
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Products Section */}
|
{/* Products Section */}
|
||||||
@ -771,8 +873,8 @@ export default function Admin() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Stock *</label>
|
<label>Stock (Optional)</label>
|
||||||
<input type="number" name="stock" value={formData.stock} onChange={handleChange} required />
|
<input type="number" name="stock" value={formData.stock} onChange={handleChange} placeholder="Leave empty to hide from customers" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
<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' }}>
|
<td style={{ padding: '0.75rem', border: '1px solid #ddd', fontSize: '0.85em', color: '#666' }}>
|
||||||
{product.slug || '-'}
|
{product.slug || '-'}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>${product.price}</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.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_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' }}>{product.is_on_sale ? '✓' : '-'}</td>
|
||||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
||||||
@ -1219,6 +1321,199 @@ export default function Admin() {
|
|||||||
</div>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export default function Cart() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>${(item.product.discount_price || item.product.price).toFixed(2)}</td>
|
<td>₪{(item.product.discount_price || item.product.price).toFixed(2)}</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="quantity-control">
|
<div className="quantity-control">
|
||||||
<button onClick={() => updateQuantity(index, item.quantity - 1)}>−</button>
|
<button onClick={() => updateQuantity(index, item.quantity - 1)}>−</button>
|
||||||
@ -64,7 +64,7 @@ export default function Cart() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<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>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
@ -85,7 +85,7 @@ export default function Cart() {
|
|||||||
<div className="summary-rows">
|
<div className="summary-rows">
|
||||||
<div className="summary-row">
|
<div className="summary-row">
|
||||||
<span>Subtotal:</span>
|
<span>Subtotal:</span>
|
||||||
<span>${total.toFixed(2)}</span>
|
<span>₪{total.toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="summary-row">
|
<div className="summary-row">
|
||||||
<span>Shipping:</span>
|
<span>Shipping:</span>
|
||||||
@ -93,11 +93,11 @@ export default function Cart() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="summary-row">
|
<div className="summary-row">
|
||||||
<span>Tax:</span>
|
<span>Tax:</span>
|
||||||
<span>${(total * 0.1).toFixed(2)}</span>
|
<span>₪{(total * 0.1).toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="summary-row total">
|
<div className="summary-row total">
|
||||||
<span>Total:</span>
|
<span>Total:</span>
|
||||||
<span>${(total + 10 + total * 0.1).toFixed(2)}</span>
|
<span>₪{(total + 10 + total * 0.1).toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -119,12 +119,12 @@ export default function Checkout() {
|
|||||||
{cart.map((item, index) => (
|
{cart.map((item, index) => (
|
||||||
<div key={index} className="summary-item">
|
<div key={index} className="summary-item">
|
||||||
<span>{item.product.name} x{item.quantity}</span>
|
<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>
|
</div>
|
||||||
<div className="summary-total">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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' }}>{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' }}>{getCategoryName(model.category_id)}</td>
|
||||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
<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>
|
||||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd', fontSize: '0.85em' }}>
|
<td style={{ padding: '0.75rem', border: '1px solid #ddd', fontSize: '0.85em' }}>
|
||||||
{model.sizes ? model.sizes.join(', ') : '-'}
|
{model.sizes ? model.sizes.join(', ') : '-'}
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export default function Orders() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="detail-row">
|
<div className="detail-row">
|
||||||
<span>Total Amount:</span>
|
<span>Total Amount:</span>
|
||||||
<span>${order.total_amount.toFixed(2)}</span>
|
<span>₪{order.total_amount.toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="detail-row">
|
<div className="detail-row">
|
||||||
<span>Items:</span>
|
<span>Items:</span>
|
||||||
@ -74,7 +74,7 @@ export default function Orders() {
|
|||||||
<p>{item.product.name}</p>
|
<p>{item.product.name}</p>
|
||||||
<p>Qty: {item.quantity}</p>
|
<p>Qty: {item.quantity}</p>
|
||||||
</div>
|
</div>
|
||||||
<p>${item.price.toFixed(2)}</p>
|
<p>₪{item.price.toFixed(2)}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -141,11 +141,11 @@ export default function ProductDetail() {
|
|||||||
<div className="price">
|
<div className="price">
|
||||||
{product.discount_price ? (
|
{product.discount_price ? (
|
||||||
<>
|
<>
|
||||||
<span className="original">${product.price.toFixed(2)}</span>
|
<span className="original">₪{product.price.toFixed(2)}</span>
|
||||||
<span className="current">${product.discount_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>
|
</div>
|
||||||
|
|
||||||
@ -177,6 +177,7 @@ export default function ProductDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{product.stock !== null && (
|
||||||
<div className="stock-info">
|
<div className="stock-info">
|
||||||
{product.stock > 0 ? (
|
{product.stock > 0 ? (
|
||||||
<span className="in-stock">✓ In Stock ({product.stock} available)</span>
|
<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>
|
<span className="out-of-stock">✗ Out of Stock</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="action-buttons">
|
<div className="action-buttons">
|
||||||
<button
|
<button
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user