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:
Alvin
2025-06-10 11:38:27 +02:00
parent ec07b89dab
commit 1ac852f2a1
5 changed files with 329 additions and 145 deletions

14
client/public/favicon.svg Normal file
View 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

View File

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

View File

@@ -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}>
<DialogContent>
<TextField
fullWidth
label="Name"
name="name"
value={formData.name}
onChange={handleChange}
margin="normal"
required
/>
<TextField
fullWidth
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
margin="normal"
required
/>
<TextField
fullWidth
label="Phone"
name="phone"
value={formData.phone}
onChange={handleChange}
margin="normal"
required
/>
<TextField
fullWidth
label="Address"
name="address"
value={formData.address}
onChange={handleChange}
margin="normal"
multiline
rows={2}
/>
<FormControl fullWidth margin="normal" required>
<InputLabel>Car Model</InputLabel>
<Select
name="carModel"
value={formData.carModel}
onChange={handleChange}
label="Car Model"
>
{carModels.map((model) => (
<MenuItem key={model.value} value={model.value}>
{model.label}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
fullWidth
label="Car Year"
name="carYear"
value={formData.carYear}
onChange={handleChange}
margin="normal"
required
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button type="submit" variant="contained" color="primary">
{selectedCustomer ? 'Update' : 'Add'}
</Button>
</DialogActions>
</form>
<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="Full Name"
name="name"
value={formData.name}
onChange={handleInputChange}
required
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Email Address"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
required
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Phone Number"
name="phone"
value={formData.phone}
onChange={handleInputChange}
required
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Address"
name="address"
value={formData.address}
onChange={handleInputChange}
required
/>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth required>
<InputLabel>Car Model</InputLabel>
<Select
name="carModel"
value={formData.carModel}
onChange={handleInputChange}
label="Car Model"
>
{carModels.map((model) => (
<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={handleInputChange}
required
/>
</Grid>
</Grid>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleSubmit} variant="contained" color="primary">
{selectedCustomer ? 'Update' : 'Add'}
</Button>
</DialogActions>
</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>
);

View File

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

View File

@@ -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');
}
});