mirror of
https://github.com/Alvin-Zilverstand/challenge-11.git
synced 2026-03-06 11:06:21 +01:00
Update customer management functionality by adding modification support, enhancing the customer model to include modifications, and improving the UI for customer and modification forms. Refactor API routes for better error handling and streamline customer data management.
This commit is contained in:
14
client/public/favicon.svg
Normal file
14
client/public/favicon.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Car body -->
|
||||
<path d="M6 20C6 18.8954 6.89543 18 8 18H24C25.1046 18 26 18.8954 26 20V24C26 25.1046 25.1046 26 24 26H8C6.89543 26 6 25.1046 6 24V20Z" fill="#ff3d00"/>
|
||||
<!-- Car top -->
|
||||
<path d="M10 14C10 12.8954 10.8954 12 12 12H20C21.1046 12 22 12.8954 22 14V18H10V14Z" fill="#ff3d00"/>
|
||||
<!-- Wheels -->
|
||||
<circle cx="10" cy="22" r="3" fill="#212121"/>
|
||||
<circle cx="22" cy="22" r="3" fill="#212121"/>
|
||||
<!-- Speed lines -->
|
||||
<path d="M4 16L8 16" stroke="#ff3d00" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M4 18L8 18" stroke="#ff3d00" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M4 20L8 20" stroke="#ff3d00" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 826 B |
@@ -2,14 +2,14 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="%PUBLIC_URL%/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
content="Car Tuning & Modification Management System"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.svg" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
@@ -24,7 +24,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>AutoTune Pro | Car Tuning Management</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -22,40 +22,47 @@ import {
|
||||
Select,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { Edit as EditIcon, Delete as DeleteIcon, Add as AddIcon } from '@mui/icons-material';
|
||||
import axios from 'axios';
|
||||
|
||||
const carModels = [
|
||||
{ value: 'bmw_m3', label: 'BMW M3' },
|
||||
{ value: 'bmw_m4', label: 'BMW M4' },
|
||||
{ value: 'audi_rs3', label: 'Audi RS3' },
|
||||
{ value: 'audi_rs4', label: 'Audi RS4' },
|
||||
{ value: 'mercedes_c63', label: 'Mercedes-AMG C63' },
|
||||
{ value: 'mercedes_e63', label: 'Mercedes-AMG E63' },
|
||||
{ value: 'volkswagen_golf_r', label: 'Volkswagen Golf R' },
|
||||
{ value: 'volkswagen_arteon_r', label: 'Volkswagen Arteon R' },
|
||||
{ value: 'toyota_supra', label: 'Toyota Supra' },
|
||||
{ value: 'nissan_gtr', label: 'Nissan GT-R' },
|
||||
{ value: 'porsche_911', label: 'Porsche 911' },
|
||||
{ value: 'porsche_cayman', label: 'Porsche Cayman' },
|
||||
{ value: 'honda_civic_type_r', label: 'Honda Civic Type R' },
|
||||
{ value: 'subaru_wrx_sti', label: 'Subaru WRX STI' },
|
||||
{ value: 'mitsubishi_evo', label: 'Mitsubishi Lancer Evolution' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
'BMW M3',
|
||||
'BMW M4',
|
||||
'Audi RS3',
|
||||
'Audi RS4',
|
||||
'Mercedes-AMG C63',
|
||||
'Mercedes-AMG E63',
|
||||
'Volkswagen Golf R',
|
||||
'Volkswagen Arteon R',
|
||||
'Toyota Supra',
|
||||
'Nissan GT-R',
|
||||
'Porsche 911',
|
||||
'Porsche Cayman',
|
||||
'Honda Civic Type R',
|
||||
'Subaru WRX STI',
|
||||
'Mitsubishi Lancer Evolution',
|
||||
'Other'
|
||||
];
|
||||
|
||||
const categories = [
|
||||
'Engine',
|
||||
'Exhaust',
|
||||
'Suspension',
|
||||
'Brakes',
|
||||
'Wheels'
|
||||
];
|
||||
|
||||
// Create axios instance with default config
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:5000/api',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
baseURL: 'http://localhost:5000/api'
|
||||
});
|
||||
|
||||
// Add request interceptor to add token to all requests
|
||||
// Add request interceptor to include token
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
@@ -72,17 +79,24 @@ api.interceptors.request.use(
|
||||
const CustomerManagement = () => {
|
||||
const [customers, setCustomers] = useState([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openModDialog, setOpenModDialog] = useState(false);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
carModel: '',
|
||||
carYear: '',
|
||||
carYear: ''
|
||||
});
|
||||
const [modificationData, setModificationData] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
price: '',
|
||||
category: ''
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchCustomers();
|
||||
@@ -99,32 +113,56 @@ const CustomerManagement = () => {
|
||||
|
||||
const handleOpen = (customer = null) => {
|
||||
if (customer) {
|
||||
setSelectedCustomer(customer);
|
||||
setFormData(customer);
|
||||
setSelectedCustomer(customer);
|
||||
} else {
|
||||
setSelectedCustomer(null);
|
||||
setFormData({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
carModel: '',
|
||||
carYear: '',
|
||||
carYear: ''
|
||||
});
|
||||
setSelectedCustomer(null);
|
||||
}
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenModDialog = (customer) => {
|
||||
setSelectedCustomer(customer);
|
||||
setModificationData({
|
||||
name: '',
|
||||
description: '',
|
||||
price: '',
|
||||
category: ''
|
||||
});
|
||||
setOpenModDialog(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const handleCloseModDialog = () => {
|
||||
setOpenModDialog(false);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
const handleModInputChange = (e) => {
|
||||
setModificationData({
|
||||
...modificationData,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
@@ -145,6 +183,18 @@ const CustomerManagement = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddModification = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api.put(`/customers/${selectedCustomer._id}/modifications`, modificationData);
|
||||
setSuccess('Modification added successfully');
|
||||
fetchCustomers();
|
||||
handleCloseModDialog();
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message || 'An error occurred');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (window.confirm('Are you sure you want to delete this customer?')) {
|
||||
try {
|
||||
@@ -159,8 +209,8 @@ const CustomerManagement = () => {
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
|
||||
<Typography variant="h4" component="h1" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
Customer Management
|
||||
</Typography>
|
||||
<Button
|
||||
@@ -173,17 +223,8 @@ const CustomerManagement = () => {
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert severity="success" sx={{ mb: 2 }}>
|
||||
{success}
|
||||
</Alert>
|
||||
)}
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
{success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
@@ -194,6 +235,7 @@ const CustomerManagement = () => {
|
||||
<TableCell>Phone</TableCell>
|
||||
<TableCell>Car Model</TableCell>
|
||||
<TableCell>Car Year</TableCell>
|
||||
<TableCell>Modifications</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -205,6 +247,24 @@ const CustomerManagement = () => {
|
||||
<TableCell>{customer.phone}</TableCell>
|
||||
<TableCell>{customer.carModel}</TableCell>
|
||||
<TableCell>{customer.carYear}</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => handleOpenModDialog(customer)}
|
||||
>
|
||||
Add Modification
|
||||
</Button>
|
||||
{customer.modifications && customer.modifications.length > 0 && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
{customer.modifications.map((mod, index) => (
|
||||
<Typography key={index} variant="body2">
|
||||
{mod.name} - {mod.category}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton onClick={() => handleOpen(customer)} color="primary">
|
||||
<EditIcon />
|
||||
@@ -219,82 +279,157 @@ const CustomerManagement = () => {
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Customer Form Dialog */}
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
{selectedCustomer ? 'Edit Customer' : 'Add New Customer'}
|
||||
</DialogTitle>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogTitle>{selectedCustomer ? 'Edit Customer' : 'Add Customer'}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Name"
|
||||
label="Full Name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Email"
|
||||
label="Email Address"
|
||||
name="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Phone"
|
||||
label="Phone Number"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Address"
|
||||
name="address"
|
||||
value={formData.address}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
multiline
|
||||
rows={2}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
<FormControl fullWidth margin="normal" required>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth required>
|
||||
<InputLabel>Car Model</InputLabel>
|
||||
<Select
|
||||
name="carModel"
|
||||
value={formData.carModel}
|
||||
onChange={handleChange}
|
||||
onChange={handleInputChange}
|
||||
label="Car Model"
|
||||
>
|
||||
{carModels.map((model) => (
|
||||
<MenuItem key={model.value} value={model.value}>
|
||||
{model.label}
|
||||
<MenuItem key={model} value={model}>
|
||||
{model}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Car Year"
|
||||
name="carYear"
|
||||
type="number"
|
||||
value={formData.carYear}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
<Button type="submit" variant="contained" color="primary">
|
||||
<Button onClick={handleSubmit} variant="contained" color="primary">
|
||||
{selectedCustomer ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
||||
{/* Modification Form Dialog */}
|
||||
<Dialog open={openModDialog} onClose={handleCloseModDialog} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Add Modification</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box component="form" onSubmit={handleAddModification} sx={{ mt: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Modification Name"
|
||||
name="name"
|
||||
value={modificationData.name}
|
||||
onChange={handleModInputChange}
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Description"
|
||||
name="description"
|
||||
multiline
|
||||
rows={3}
|
||||
value={modificationData.description}
|
||||
onChange={handleModInputChange}
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Price"
|
||||
name="price"
|
||||
type="number"
|
||||
value={modificationData.price}
|
||||
onChange={handleModInputChange}
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth required>
|
||||
<InputLabel>Category</InputLabel>
|
||||
<Select
|
||||
name="category"
|
||||
value={modificationData.category}
|
||||
onChange={handleModInputChange}
|
||||
label="Category"
|
||||
>
|
||||
{categories.map((category) => (
|
||||
<MenuItem key={category} value={category}>
|
||||
{category}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseModDialog}>Cancel</Button>
|
||||
<Button onClick={handleAddModification} variant="contained" color="primary">
|
||||
Add Modification
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ const customerSchema = new mongoose.Schema({
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
carModel: {
|
||||
@@ -28,10 +29,31 @@ const customerSchema = new mongoose.Schema({
|
||||
trim: true
|
||||
},
|
||||
carYear: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
modifications: [{
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
dateAdded: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}],
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
@@ -48,4 +70,6 @@ customerSchema.pre('save', function(next) {
|
||||
next();
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Customer', customerSchema);
|
||||
const Customer = mongoose.model('Customer', customerSchema);
|
||||
|
||||
module.exports = Customer;
|
||||
@@ -9,7 +9,7 @@ router.get('/', auth, async (req, res) => {
|
||||
const customers = await Customer.find().sort({ name: 1 });
|
||||
res.json(customers);
|
||||
} catch (err) {
|
||||
res.status(500).json({ message: err.message });
|
||||
res.status(500).send('Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,24 +22,16 @@ router.get('/:id', auth, async (req, res) => {
|
||||
}
|
||||
res.json(customer);
|
||||
} catch (err) {
|
||||
res.status(500).json({ message: err.message });
|
||||
res.status(500).send('Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
// Create customer
|
||||
router.post('/', auth, async (req, res) => {
|
||||
const customer = new Customer({
|
||||
name: req.body.name,
|
||||
email: req.body.email,
|
||||
phone: req.body.phone,
|
||||
address: req.body.address,
|
||||
carModel: req.body.carModel,
|
||||
carYear: req.body.carYear,
|
||||
});
|
||||
|
||||
try {
|
||||
const newCustomer = await customer.save();
|
||||
res.status(201).json(newCustomer);
|
||||
const customer = new Customer(req.body);
|
||||
await customer.save();
|
||||
res.status(201).json(customer);
|
||||
} catch (err) {
|
||||
res.status(400).json({ message: err.message });
|
||||
}
|
||||
@@ -53,9 +45,28 @@ router.put('/:id', auth, async (req, res) => {
|
||||
return res.status(404).json({ message: 'Customer not found' });
|
||||
}
|
||||
|
||||
Object.assign(customer, req.body);
|
||||
const updatedCustomer = await customer.save();
|
||||
res.json(updatedCustomer);
|
||||
Object.keys(req.body).forEach(key => {
|
||||
customer[key] = req.body[key];
|
||||
});
|
||||
|
||||
await customer.save();
|
||||
res.json(customer);
|
||||
} catch (err) {
|
||||
res.status(400).json({ message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Add modification to customer
|
||||
router.put('/:id/modifications', auth, async (req, res) => {
|
||||
try {
|
||||
const customer = await Customer.findById(req.params.id);
|
||||
if (!customer) {
|
||||
return res.status(404).json({ message: 'Customer not found' });
|
||||
}
|
||||
|
||||
customer.modifications.push(req.body);
|
||||
await customer.save();
|
||||
res.json(customer);
|
||||
} catch (err) {
|
||||
res.status(400).json({ message: err.message });
|
||||
}
|
||||
@@ -72,7 +83,7 @@ router.delete('/:id', auth, async (req, res) => {
|
||||
await customer.remove();
|
||||
res.json({ message: 'Customer deleted' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ message: err.message });
|
||||
res.status(500).send('Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user