332 lines
11 KiB
JavaScript
332 lines
11 KiB
JavaScript
import React, { useState, useEffect, useContext } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import api from '../api'
|
|
import { AuthContext } from '../context/AuthContext'
|
|
import '../styles/global.css'
|
|
|
|
export default function Models() {
|
|
const navigate = useNavigate()
|
|
const { user } = useContext(AuthContext)
|
|
const [models, setModels] = useState([])
|
|
const [categories, setCategories] = useState([])
|
|
const [brands, setBrands] = useState([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [showForm, setShowForm] = useState(false)
|
|
const [editingModel, setEditingModel] = useState(null)
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
category_id: '',
|
|
brand: '',
|
|
base_price: '',
|
|
sizes: '',
|
|
description: '',
|
|
})
|
|
|
|
// Redirect if not admin
|
|
useEffect(() => {
|
|
if (!user?.is_admin) {
|
|
navigate('/')
|
|
}
|
|
}, [user, navigate])
|
|
|
|
useEffect(() => {
|
|
fetchModels()
|
|
fetchCategories()
|
|
fetchBrands()
|
|
}, [])
|
|
|
|
const fetchModels = async () => {
|
|
try {
|
|
const response = await api.get('/models')
|
|
setModels(response.data)
|
|
} catch (error) {
|
|
console.error('Error fetching models:', 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 fetchBrands = async () => {
|
|
try {
|
|
const response = await api.get('/brands')
|
|
setBrands(response.data.map(b => b.name).sort())
|
|
} catch (error) {
|
|
console.error('Error fetching brands:', error)
|
|
}
|
|
}
|
|
|
|
const handleChange = (e) => {
|
|
const { name, value } = e.target
|
|
setFormData({ ...formData, [name]: value })
|
|
}
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault()
|
|
|
|
const modelData = {
|
|
...formData,
|
|
category_id: parseInt(formData.category_id),
|
|
base_price: formData.base_price ? parseFloat(formData.base_price) : null,
|
|
sizes: formData.sizes ? formData.sizes.split(',').map(s => s.trim()) : [],
|
|
}
|
|
|
|
try {
|
|
if (editingModel) {
|
|
await api.put(`/models/${editingModel.id}`, modelData)
|
|
alert('Model updated successfully!')
|
|
} else {
|
|
await api.post('/models', modelData)
|
|
alert('Model created successfully!')
|
|
}
|
|
setShowForm(false)
|
|
setEditingModel(null)
|
|
resetForm()
|
|
fetchModels()
|
|
} catch (error) {
|
|
console.error('Error saving model:', error)
|
|
alert('Error saving model: ' + (error.response?.data?.detail || 'Unknown error'))
|
|
}
|
|
}
|
|
|
|
const handleEdit = (model) => {
|
|
setEditingModel(model)
|
|
setFormData({
|
|
name: model.name || '',
|
|
category_id: model.category_id || '',
|
|
brand: model.brand || '',
|
|
base_price: model.base_price || '',
|
|
sizes: Array.isArray(model.sizes) ? model.sizes.join(', ') : '',
|
|
description: model.description || '',
|
|
})
|
|
setShowForm(true)
|
|
setTimeout(() => {
|
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
}, 100)
|
|
}
|
|
|
|
const handleDelete = 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 resetForm = () => {
|
|
setFormData({
|
|
name: '',
|
|
category_id: '',
|
|
brand: '',
|
|
base_price: '',
|
|
sizes: '',
|
|
description: '',
|
|
})
|
|
}
|
|
|
|
const handleCancel = () => {
|
|
setShowForm(false)
|
|
setEditingModel(null)
|
|
resetForm()
|
|
}
|
|
|
|
if (!user?.is_admin) {
|
|
return null
|
|
}
|
|
|
|
const getCategoryName = (categoryId) => {
|
|
const category = categories.find(c => c.id === categoryId)
|
|
return category ? category.name : 'Unknown'
|
|
}
|
|
|
|
return (
|
|
<div className="models-page" style={{ padding: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
|
<h1>Manage Models</h1>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={() => {
|
|
setShowForm(!showForm)
|
|
setEditingModel(null)
|
|
resetForm()
|
|
}}
|
|
>
|
|
{showForm ? 'Cancel' : '+ Add New Model'}
|
|
</button>
|
|
</div>
|
|
|
|
{showForm && (
|
|
<div style={{ backgroundColor: '#f5f5f5', padding: '2rem', borderRadius: '8px', marginBottom: '2rem' }}>
|
|
<h2>{editingModel ? 'Edit Model' : 'Create New Model'}</h2>
|
|
<form onSubmit={handleSubmit}>
|
|
<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={formData.name}
|
|
onChange={handleChange}
|
|
placeholder="9060"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="form-group">
|
|
<label>Brand *</label>
|
|
<select
|
|
name="brand"
|
|
value={formData.brand}
|
|
onChange={handleChange}
|
|
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={formData.category_id} onChange={handleChange} 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={formData.base_price}
|
|
onChange={handleChange}
|
|
placeholder="129.99"
|
|
/>
|
|
</div>
|
|
|
|
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
|
<label>Default Sizes (comma separated)</label>
|
|
<input
|
|
type="text"
|
|
name="sizes"
|
|
value={formData.sizes}
|
|
onChange={handleChange}
|
|
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={formData.description}
|
|
onChange={handleChange}
|
|
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={handleCancel}>
|
|
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' }}>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', fontSize: '0.85em' }}>
|
|
{model.sizes ? model.sizes.join(', ') : '-'}
|
|
</td>
|
|
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
|
<button
|
|
onClick={() => handleEdit(model)}
|
|
style={{
|
|
marginRight: '0.5rem',
|
|
padding: '0.25rem 0.75rem',
|
|
backgroundColor: '#4CAF50',
|
|
color: 'white',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
cursor: 'pointer'
|
|
}}
|
|
>
|
|
Edit
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(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>
|
|
)
|
|
}
|