import React, { useState, useEffect, useContext } from 'react' import { useNavigate, Link } from 'react-router-dom' import api from '../api' import { AuthContext } from '../context/AuthContext' import Toast from '../components/Toast' import '../styles/global.css' export default function Admin() { const navigate = useNavigate() const { user, token } = useContext(AuthContext) const [toast, setToast] = useState(null) const [products, setProducts] = useState([]) const [categories, setCategories] = useState([]) const [loading, setLoading] = useState(true) const [showForm, setShowForm] = useState(false) const [editingProduct, setEditingProduct] = useState(null) const [formData, setFormData] = useState({ name: '', slug: '', description: '', price: '', discount_price: '', category_id: '', model_id: '', gender: 'men', brand: '', sizes: '', stock: '', images: '', is_featured: false, is_on_sale: false, override_price: '', override_sizes: '', }) const [uploadingImage, setUploadingImage] = useState(false) const [uploadedImages, setUploadedImages] = useState([]) const [models, setModels] = useState([]) const [activeTab, setActiveTab] = useState('products') // products, categories, brands, models, messages const [showCategoryForm, setShowCategoryForm] = useState(false) const [editingCategory, setEditingCategory] = useState(null) const [categoryFormData, setCategoryFormData] = useState({ name: '', slug: '', description: '', image: '', }) const [searchQuery, setSearchQuery] = useState('') const [filterBrand, setFilterBrand] = useState('') const [filterCategory, setFilterCategory] = useState('') const [filterModel, setFilterModel] = useState('') const [brands, setBrands] = useState([]) const [allProducts, setAllProducts] = useState([]) // Store all products for filtering const [showBrandForm, setShowBrandForm] = useState(false) 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: '', }) // Contact Messages state const [contactMessages, setContactMessages] = useState([]) const [filteredMessages, setFilteredMessages] = useState([]) const [unreadCount, setUnreadCount] = useState(0) const [messageFilter, setMessageFilter] = useState('all') // all, new, read, replied const [selectedMessage, setSelectedMessage] = useState(null) const [showMessageModal, setShowMessageModal] = useState(false) const [messageNotes, setMessageNotes] = useState('') // Redirect if not admin useEffect(() => { if (!user?.is_admin) { navigate('/') } }, [user, navigate]) useEffect(() => { fetchProducts() fetchCategories() fetchModels() fetchBrands() fetchContactMessages() fetchUnreadCount() }, []) const fetchProducts = async () => { try { const response = await api.get('/products') setAllProducts(response.data) setProducts(response.data) } catch (error) { console.error('Error fetching products:', error) } finally { setLoading(false) } } const fetchCategories = async () => { try { const response = await api.get('/categories') setCategories(response.data) } catch (error) { console.error('Error fetching categories:', error) } } const fetchModels = async () => { try { const response = await api.get('/models') setModels(response.data) } catch (error) { console.error('Error fetching models:', error) } } const fetchBrands = async () => { try { const response = await api.get('/brands') setBrands(response.data.map(b => b.name).sort()) setBrandsList(response.data) } catch (error) { console.error('Error fetching brands:', error) } } const fetchContactMessages = async () => { try { const response = await api.get('/admin/contact-messages') setContactMessages(response.data) setFilteredMessages(response.data) } catch (error) { console.error('Error fetching contact messages:', error) } } const fetchUnreadCount = async () => { try { const response = await api.get('/admin/contact-messages/unread-count') setUnreadCount(response.data.unread_count) } catch (error) { console.error('Error fetching unread count:', error) } } // Filter contact messages based on status useEffect(() => { if (messageFilter === 'all') { setFilteredMessages(contactMessages) } else { setFilteredMessages(contactMessages.filter(m => m.status === messageFilter)) } }, [messageFilter, contactMessages]) // Filter products based on search and filters useEffect(() => { let filtered = [...allProducts] // Search filter if (searchQuery) { filtered = filtered.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()) || p.brand?.toLowerCase().includes(searchQuery.toLowerCase()) || p.description?.toLowerCase().includes(searchQuery.toLowerCase()) ) } // Brand filter if (filterBrand) { filtered = filtered.filter(p => p.brand === filterBrand) } // Category filter if (filterCategory) { filtered = filtered.filter(p => p.category_id === parseInt(filterCategory)) } // Model filter if (filterModel) { filtered = filtered.filter(p => p.model_id === parseInt(filterModel)) } setProducts(filtered) }, [searchQuery, filterBrand, filterCategory, filterModel, allProducts]) const handleChange = (e) => { const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value setFormData({ ...formData, [e.target.name]: value, }) } const handleImageUpload = async (e) => { const files = e.target.files if (!files || files.length === 0) return setUploadingImage(true) const newImages = [...uploadedImages] try { for (let i = 0; i < files.length; i++) { const file = files[i] const formDataUpload = new FormData() formDataUpload.append('file', file) const response = await api.post('/products/upload-image', formDataUpload, { headers: { 'Content-Type': 'multipart/form-data', }, }) // Backend now returns full URLs newImages.push(response.data.url) } setUploadedImages(newImages) // Update form data with new images const currentImages = formData.images ? formData.images.split(',').map(s => s.trim()).filter(s => s) : [] const allImages = [...currentImages, ...newImages.map(url => url)] setFormData({ ...formData, images: allImages.join(', ') }) setToast({ type: 'success', message: 'Images uploaded successfully!' }) } catch (error) { console.error('Error uploading image:', error) setToast({ type: 'error', message: 'Error uploading image: ' + (error.response?.data?.detail || 'Unknown error') }) } finally { setUploadingImage(false) } } const removeImage = (imageUrl) => { const currentImages = formData.images.split(',').map(s => s.trim()).filter(s => s) const filtered = currentImages.filter(img => img !== imageUrl) setFormData({ ...formData, images: filtered.join(', ') }) setUploadedImages(uploadedImages.filter(img => img !== imageUrl)) } const handleSubmit = async (e) => { e.preventDefault() const productData = { ...formData, price: formData.price ? parseFloat(formData.price) : null, // Allow null - inherits from model 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: 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, } try { if (editingProduct) { await api.put(`/products/${editingProduct.id}`, productData) setToast({ type: 'success', message: 'Product updated successfully!' }) } else { await api.post('/products', productData) setToast({ type: 'success', message: 'Product created successfully!' }) } setShowForm(false) setEditingProduct(null) resetForm() fetchProducts() } catch (error) { console.error('Error saving product:', error) setToast({ type: 'error', message: 'Error saving product: ' + (error.response?.data?.detail || 'Unknown error') }) } } const handleEdit = (product) => { console.log('Edit clicked for product:', product) try { setEditingProduct(product) const imageList = Array.isArray(product.images) ? product.images : [] console.log('Setting form data...') setFormData({ name: product.name || '', slug: product.slug || '', description: product.description || '', price: product.price || '', discount_price: product.discount_price || '', category_id: product.category_id || '', model_id: product.model_id || '', gender: product.gender || 'men', brand: product.brand || '', sizes: Array.isArray(product.sizes) ? product.sizes.join(', ') : '', stock: product.stock || '', images: imageList.join(', '), is_featured: product.is_featured || false, is_on_sale: product.is_on_sale || false, override_price: product.override_price || '', override_sizes: Array.isArray(product.override_sizes) ? product.override_sizes.join(', ') : '', }) setUploadedImages(imageList) console.log('Showing form...') setShowForm(true) console.log('Form should now be visible') // Scroll to top to show the form setTimeout(() => { window.scrollTo({ top: 0, behavior: 'smooth' }) }, 100) } catch (error) { console.error('Error in handleEdit:', error) setToast({ type: 'error', message: 'Error loading product: ' + error.message }) } } const handleDelete = async (id) => { if (!confirm('Are you sure you want to delete this product?')) return try { await api.delete(`/products/${id}`) setToast({ type: 'success', message: 'Product deleted successfully!' }) fetchProducts() } catch (error) { console.error('Error deleting product:', error) setToast({ type: 'error', message: 'Error deleting product' }) } } const resetForm = () => { setFormData({ name: '', slug: '', description: '', price: '', discount_price: '', category_id: '', model_id: '', gender: 'men', brand: '', sizes: '', stock: '', images: '', is_featured: false, is_on_sale: false, override_price: '', override_sizes: '', }) setUploadedImages([]) } const handleCancel = () => { setShowForm(false) setEditingProduct(null) resetForm() } // Category Management Functions const handleCategoryChange = (e) => { const { name, value } = e.target setCategoryFormData({ ...categoryFormData, [name]: value }) } const handleCategoryImageUpload = async (e) => { const file = e.target.files[0] if (!file) return setUploadingImage(true) const formData = new FormData() formData.append('file', file) try { const response = await api.post('/categories/upload-image', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) // Backend now returns full URLs setCategoryFormData({ ...categoryFormData, image: response.data.url }) setToast({ type: 'success', message: 'Image uploaded successfully!' }) } catch (error) { console.error('Error uploading image:', error) setToast({ type: 'error', message: 'Error uploading image' }) } finally { setUploadingImage(false) } } const handleCategorySubmit = async (e) => { e.preventDefault() try { if (editingCategory) { await api.put(`/categories/${editingCategory.id}`, categoryFormData) setToast({ type: 'success', message: 'Category updated successfully!' }) } else { await api.post('/categories', categoryFormData) setToast({ type: 'success', message: 'Category created successfully!' }) } setShowCategoryForm(false) setEditingCategory(null) resetCategoryForm() fetchCategories() } catch (error) { console.error('Error saving category:', error) setToast({ type: 'error', message: 'Error saving category: ' + (error.response?.data?.detail || 'Unknown error') }) } } const handleEditCategory = (category) => { setEditingCategory(category) setCategoryFormData({ name: category.name || '', slug: category.slug || '', description: category.description || '', image: category.image || '', }) setShowCategoryForm(true) setTimeout(() => { window.scrollTo({ top: 0, behavior: 'smooth' }) }, 100) } const handleDeleteCategory = async (id) => { if (!confirm('Are you sure you want to delete this category?')) return try { await api.delete(`/categories/${id}`) setToast({ type: 'success', message: 'Category deleted successfully!' }) fetchCategories() } catch (error) { console.error('Error deleting category:', error) setToast({ type: 'error', message: 'Error deleting category: ' + (error.response?.data?.detail || 'Unknown error') }) } } const resetCategoryForm = () => { setCategoryFormData({ name: '', slug: '', description: '', image: '', }) } // Brand management handlers const handleAddBrand = async () => { const newBrand = brandFormData.name.trim() if (!newBrand) { setToast({ type: 'error', message: 'Please enter a brand name' }) return } try { await api.post('/brands', { name: newBrand }) setShowBrandForm(false) setBrandFormData({ name: '' }) fetchBrands() setToast({ type: 'success', message: 'Brand added successfully!' }) } catch (error) { console.error('Error adding brand:', error) setToast({ type: 'error', message: 'Error adding brand: ' + (error.response?.data?.detail || 'Unknown error') }) } } const handleUpdateBrand = async () => { const oldBrand = editingBrand const newBrand = brandFormData.name.trim() if (!newBrand) { setToast({ type: 'error', message: 'Please enter a brand name' }) return } if (oldBrand === newBrand) { setShowBrandForm(false) setEditingBrand(null) return } if (!confirm(`Update brand from "${oldBrand}" to "${newBrand}"?`)) { return } try { // Find the brand ID const brandToUpdate = brandsList.find(b => b.name === oldBrand) if (!brandToUpdate) { setToast({ type: 'error', message: 'Brand not found' }) return } // Update the brand await api.put(`/brands/${brandToUpdate.id}`, { name: newBrand }) // Update all products with this brand const productsToUpdate = allProducts.filter(p => p.brand === oldBrand) for (const product of productsToUpdate) { await api.put(`/products/${product.id}`, { ...product, brand: newBrand }) } // Update all models with this brand const modelsToUpdate = models.filter(m => m.brand === oldBrand) for (const model of modelsToUpdate) { await api.put(`/models/${model.id}`, { ...model, brand: newBrand }) } setShowBrandForm(false) setEditingBrand(null) setBrandFormData({ name: '' }) // Refresh data fetchBrands() fetchProducts() fetchModels() setToast({ type: 'success', message: 'Brand updated successfully!' }) } catch (error) { console.error('Error updating brand:', error) setToast({ type: 'error', message: 'Error updating brand: ' + (error.response?.data?.detail || 'Unknown error') }) } } const handleDeleteBrand = async (brand) => { const productCount = allProducts.filter(p => p.brand === brand).length const modelCount = models.filter(m => m.brand === brand).length if (productCount > 0 || modelCount > 0) { setToast({ type: 'error', message: `Cannot delete "${brand}". It is used in ${productCount} product(s) and ${modelCount} model(s).` }) return } if (!confirm(`Are you sure you want to delete the brand "${brand}"?`)) { return } try { // Find the brand ID const brandToDelete = brandsList.find(b => b.name === brand) if (!brandToDelete) { setToast({ type: 'error', message: 'Brand not found' }) return } await api.delete(`/brands/${brandToDelete.id}`) fetchBrands() setToast({ type: 'success', message: 'Brand deleted successfully!' }) } catch (error) { console.error('Error deleting brand:', error) setToast({ type: 'error', message: 'Error deleting brand: ' + (error.response?.data?.detail || 'Unknown error') }) } } // 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) setToast({ type: 'success', message: 'Model updated successfully!' }) } else { await api.post('/models', modelData) setToast({ type: 'success', message: 'Model created successfully!' }) } setShowModelForm(false) setEditingModel(null) resetModelForm() fetchModels() } catch (error) { console.error('Error saving model:', error) setToast({ type: 'error', message: '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}`) setToast({ type: 'success', message: 'Model deleted successfully!' }) fetchModels() } catch (error) { console.error('Error deleting model:', error) setToast({ type: 'error', message: '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() } // Contact Message Handlers const handleUpdateMessage = async (messageId) => { try { await api.put(`/admin/contact-messages/${messageId}`, { status: selectedMessage.status, admin_notes: messageNotes, is_read: true }) setToast({ type: 'success', message: 'Message updated successfully!' }) setShowMessageModal(false) fetchContactMessages() fetchUnreadCount() } catch (error) { console.error('Error updating message:', error) setToast({ type: 'error', message: 'Error updating message: ' + (error.response?.data?.detail || 'Unknown error') }) } } const handleDeleteMessage = async (messageId) => { if (!confirm('Are you sure you want to delete this message?')) return try { await api.delete(`/admin/contact-messages/${messageId}`) setToast({ type: 'success', message: 'Message deleted successfully!' }) fetchContactMessages() fetchUnreadCount() if (showMessageModal) { setShowMessageModal(false) } } catch (error) { console.error('Error deleting message:', error) setToast({ type: 'error', message: 'Error deleting message: ' + (error.response?.data?.detail || 'Unknown error') }) } } const getCategoryName = (categoryId) => { const category = categories.find(c => c.id === categoryId) return category ? category.name : 'Unknown' } if (!user?.is_admin) { return null } return (
Loading...
) : (| ID | Name | Brand | Slug | Price | Stock | Featured | On Sale | Actions |
|---|---|---|---|---|---|---|---|---|
| {product.id} | {product.name} | {product.brand} | {product.slug || '-'} | ₪{product.price} | {product.stock ?? 'Hidden'} | {product.is_featured ? '✓' : '-'} | {product.is_on_sale ? '✓' : '-'} |
Loading...
) : (Slug: {category.slug}
{category.description && ({category.description}
)}{productCount} product{productCount !== 1 ? 's' : ''}
{modelCount} model{modelCount !== 1 ? 's' : ''}
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(', ') : '-'} |
No messages found.
) : (| ID | Name | Phone | Subject | Date | Status | Actions | |
|---|---|---|---|---|---|---|---|
| {!msg.is_read && ●} {msg.id} | {msg.full_name} | {msg.email} | {msg.phone || '-'} | {msg.subject} | {new Date(msg.created_at).toLocaleDateString()} | {msg.status.toUpperCase()} |